champollion 0.3.3 → 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 +52 -37
- 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 +51 -3
- package/lib/cards/remote.js +15 -0
- package/lib/cards/search-names.js +178 -0
- package/lib/command-help.js +289 -88
- 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 +649 -130
- 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 +16 -10
- 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 +197 -38
- 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 +193 -106
- package/lib/seal.mjs +6 -5
- package/lib/sealed-qualifier.mjs +2 -2
- 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 +3 -2
- package/shared/CORPORA-CARDS.md +2 -0
- package/shared/DATA-SOVEREIGNTY.md +19 -20
- package/shared/LANGUAGE-CARD-FIELDS.md +1 -1
- package/shared/cards-fallback.json +1 -1
- package/shared/catalogue/card-config.json +1 -1
- package/shared/curated-orthography-conventions.json +26 -8
- package/shared/docent/faq.en.json +14 -16
- package/shared/docent/system-prompt.md +17 -19
- package/shared/explainers/tc-features.json +15 -15
- package/shared/gettext-plural-forms.json +45 -0
- package/shared/human-services.json +1 -1
- 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 +20 -10
- package/shared/schemas/human-services.schema.json +2 -2
- package/shared/schemas/language-card.schema.json +1 -1
- package/shared/schemas/method-card.schema.json +1 -1
- 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 -11333
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shared-output-seed.js — what a locale's shared-output index starts a sync
|
|
3
|
+
* with: the translations already on disk that this run leaves alone.
|
|
4
|
+
*
|
|
5
|
+
* WHY: `verify` checks one locale's whole state for a model repeating one
|
|
6
|
+
* memorized sentence (lib/verify.js sharedOutputErrors): every key value of
|
|
7
|
+
* every file AND every Markdown page, counted together. Sync's gate
|
|
8
|
+
* (lib/validate.js SharedOutputIndex) saw less — only what this run accepted,
|
|
9
|
+
* plus, per key-value file, what that file kept, added when the file was
|
|
10
|
+
* reached. So a new answer repeating a sentence that sat in an untouched
|
|
11
|
+
* newsletter page, in a key-value file with nothing to translate (it returns
|
|
12
|
+
* before it seeds), or in a namespace file processed later, was written —
|
|
13
|
+
* and `verify` then flagged it (Round 10, school persona). Seeding the index
|
|
14
|
+
* with what the run leaves alone, before anything is checked, gives the gate
|
|
15
|
+
* the same scope `verify` reads.
|
|
16
|
+
*
|
|
17
|
+
* "Left alone" is what the run will not re-translate:
|
|
18
|
+
* - a key value Champollion recorded writing for the CURRENT source text
|
|
19
|
+
* (the lock's written record matches the source and the value), or one
|
|
20
|
+
* with no record whose source did not change — never an '[EN] '
|
|
21
|
+
* placeholder, a pending key, a key named for a redo, or any key under a
|
|
22
|
+
* bulk `--redo all`;
|
|
23
|
+
* - a Markdown page whose content lock says it was made from the current
|
|
24
|
+
* source, when no --redo files:/content, --force-content or --retranslate
|
|
25
|
+
* scope reaches it.
|
|
26
|
+
* Values the run replaces are left out, so their old text never counts
|
|
27
|
+
* against the new one. Read-only.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import crypto from 'node:crypto';
|
|
33
|
+
import { expectedForTarget, readLocaleFlat, lockKey, discoverLocaleLayout, loadSourceUnits } from './locale-layout.js';
|
|
34
|
+
import { originKey } from './plurals.js';
|
|
35
|
+
import { decodeWritten, shortSourceHash, valueHash, LockState } from './locale-state.js';
|
|
36
|
+
import { SharedOutputIndex, sharedOutputItems } from './validate.js';
|
|
37
|
+
import { readLock } from './hash.js';
|
|
38
|
+
import { discoverContentFiles, getTargetContentPath } from './content.js';
|
|
39
|
+
import { contentItemsFor } from './verify.js';
|
|
40
|
+
import { readContentManifest } from './content-sync.js';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Items (lib/validate.js sharedOutputItems shape) for the translations of
|
|
44
|
+
* `code` on disk that this run leaves alone.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} p
|
|
47
|
+
* @param {object} p.config - Resolved config (inputLocale, contentDir, format, fallbackPrefix, …)
|
|
48
|
+
* @param {object} p.layout - Locale layout (lib/locale-layout.js)
|
|
49
|
+
* @param {Array<object>} p.units - Source units (loadSourceUnits)
|
|
50
|
+
* @param {string} p.code - Target locale
|
|
51
|
+
* @param {{ written: object, pending: object }} p.localeState - LockState.peek(code)
|
|
52
|
+
* @param {string[]} [p.namedKeys] - Keys named for a redo (source key space, namespaced)
|
|
53
|
+
* @param {boolean} [p.bulk] - `--redo all`: every key is re-translated
|
|
54
|
+
* @param {object|null} [p.fileScope] - lib/file-scope.js (--redo files: / --files / --retranslate)
|
|
55
|
+
* @param {boolean} [p.forceContent] - --force-content / --redo content
|
|
56
|
+
* @param {string} p.cwd - Project directory
|
|
57
|
+
* @returns {Array<{ key: string, source: string, value: string }>}
|
|
58
|
+
*/
|
|
59
|
+
export function itemsLeftAlone({
|
|
60
|
+
config, layout, units, code, localeState, namedKeys = [], bulk = false, fileScope = null, forceContent = false, cwd,
|
|
61
|
+
}) {
|
|
62
|
+
const items = [];
|
|
63
|
+
const fallbackPrefix = config.fallbackPrefix || '[EN] ';
|
|
64
|
+
const named = new Set(namedKeys);
|
|
65
|
+
const written = localeState?.written || {};
|
|
66
|
+
const pending = localeState?.pending || {};
|
|
67
|
+
|
|
68
|
+
if (!bulk) {
|
|
69
|
+
for (const unit of units || []) {
|
|
70
|
+
const file = layout.fileFor(code, unit.ns);
|
|
71
|
+
if (!fs.existsSync(file.path)) continue;
|
|
72
|
+
let target;
|
|
73
|
+
try { target = readLocaleFlat(file) || {}; } catch { continue; }
|
|
74
|
+
const { flat: expected, expansion } = expectedForTarget(unit, config.inputLocale, code);
|
|
75
|
+
const changed = new Set(unit.changedKeys || []);
|
|
76
|
+
for (const [k, v] of Object.entries(target)) {
|
|
77
|
+
const src = expected[k];
|
|
78
|
+
if (typeof v !== 'string' || typeof src !== 'string' || v.startsWith(fallbackPrefix)) continue;
|
|
79
|
+
const lk = lockKey(layout, unit.ns, k);
|
|
80
|
+
const origin = originKey(k, expansion);
|
|
81
|
+
if (pending[lk] !== undefined || named.has(lk) || named.has(lockKey(layout, unit.ns, origin))) continue;
|
|
82
|
+
const record = decodeWritten(written[lk]);
|
|
83
|
+
if (record) {
|
|
84
|
+
if (record.source !== shortSourceHash(src) || record.value !== valueHash(v)) continue;
|
|
85
|
+
} else if (changed.has(origin)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
items.push(...sharedOutputItems(lk, src, v));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (config.contentDir && config.format !== 'docusaurus') {
|
|
94
|
+
const contentDir = path.resolve(cwd, config.contentDir);
|
|
95
|
+
const manifest = readContentManifest(cwd);
|
|
96
|
+
let sources = [];
|
|
97
|
+
try { sources = fs.existsSync(contentDir) ? discoverContentFiles(contentDir, config.inputLocale) : []; } catch { sources = []; }
|
|
98
|
+
const leftAlone = new Set();
|
|
99
|
+
for (const sourcePath of sources) {
|
|
100
|
+
const relPath = path.relative(contentDir, sourcePath);
|
|
101
|
+
if (!fs.existsSync(getTargetContentPath(sourcePath, code, config.inputLocale))) continue;
|
|
102
|
+
if (fileScope && !fileScope.includes(relPath)) { leftAlone.add(sourcePath); continue; }
|
|
103
|
+
if (forceContent || (fileScope && fileScope.retranslates(relPath))) continue;
|
|
104
|
+
let hash;
|
|
105
|
+
try { hash = crypto.createHash('sha256').update(fs.readFileSync(sourcePath, 'utf-8'), 'utf-8').digest('hex'); } catch { continue; }
|
|
106
|
+
if (manifest[`${relPath}:${code}`] === hash) leftAlone.add(sourcePath);
|
|
107
|
+
}
|
|
108
|
+
if (leftAlone.size > 0) items.push(...contentItemsFor(config, cwd, code, { only: (p) => leftAlone.has(p) }));
|
|
109
|
+
}
|
|
110
|
+
return items;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The shared-output index a sync of `code` starts with — ONE construction for
|
|
115
|
+
* sync's gate and for anything else that must refuse what sync refuses (the
|
|
116
|
+
* MCP translate tool with a project_dir): the sentences an earlier sync caught
|
|
117
|
+
* a model repeating (the cache's `_meta.memorized`, refused from their first
|
|
118
|
+
* source on), plus every translation of the locale on disk that the run
|
|
119
|
+
* leaves alone (itemsLeftAlone). Round 10, school persona: the translate tool
|
|
120
|
+
* returned the sentence the project already knew to be memorized, because
|
|
121
|
+
* its index held only that call's answers.
|
|
122
|
+
*
|
|
123
|
+
* @param {object} p
|
|
124
|
+
* @param {object} p.config - Resolved config
|
|
125
|
+
* @param {string} p.cwd - Project directory
|
|
126
|
+
* @param {string} p.code - Target locale
|
|
127
|
+
* @param {object|null} [p.tm] - The project's cache (lib/tm.js loadTM)
|
|
128
|
+
* @param {object} [p.layout] - Locale layout (discovered when absent)
|
|
129
|
+
* @param {Array<object>} [p.units] - Source units (loaded when absent)
|
|
130
|
+
* @param {object} [p.localeState] - LockState.peek(code) (read from the lock when absent)
|
|
131
|
+
* @param {string[]} [p.namedKeys] - Keys named for a redo this run
|
|
132
|
+
* @param {boolean} [p.bulk] - `--redo all`
|
|
133
|
+
* @param {object|null} [p.fileScope] - --redo files: / --files / --retranslate
|
|
134
|
+
* @param {boolean} [p.forceContent] - --force-content / --redo content
|
|
135
|
+
* @returns {SharedOutputIndex}
|
|
136
|
+
*/
|
|
137
|
+
export function projectSharedOutputIndex({
|
|
138
|
+
config, cwd, code, tm = null, layout = null, units = null, localeState = null,
|
|
139
|
+
namedKeys = [], bulk = false, fileScope = null, forceContent = false,
|
|
140
|
+
}) {
|
|
141
|
+
const index = new SharedOutputIndex({ protectedTerms: config.protectedTerms || [] });
|
|
142
|
+
index.markMemorized(tm?._meta?.memorized?.[code] || []);
|
|
143
|
+
let lay = layout;
|
|
144
|
+
let us = units;
|
|
145
|
+
let state = localeState;
|
|
146
|
+
// A project whose key-value files cannot be read (a Docusaurus site, a
|
|
147
|
+
// missing locales folder) still has its pages and its memorized sentences.
|
|
148
|
+
if (!lay || !us) {
|
|
149
|
+
try {
|
|
150
|
+
lay = discoverLocaleLayout(config, { cwd });
|
|
151
|
+
us = loadSourceUnits(lay);
|
|
152
|
+
} catch {
|
|
153
|
+
lay = null;
|
|
154
|
+
us = [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!state) {
|
|
158
|
+
try { state = new LockState(readLock(cwd).locales).peek(code); } catch { state = { written: {}, pending: {} }; }
|
|
159
|
+
}
|
|
160
|
+
index.add(itemsLeftAlone({
|
|
161
|
+
config, layout: lay, units: lay ? us : [], code, localeState: state, namedKeys, bulk, fileScope, forceContent, cwd,
|
|
162
|
+
}));
|
|
163
|
+
return index;
|
|
164
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* source-contexts.js — which gettext contexts (msgctxt) a project's source
|
|
3
|
+
* gives each text.
|
|
4
|
+
*
|
|
5
|
+
* Sync keys a gettext entry with a context as `msgctxt\u0004msgid`, and caches
|
|
6
|
+
* its translation under the context folded into the text (lib/tm-evict.js
|
|
7
|
+
* tmSourceText): "Cancel" the button and "Cancel" the verb are two entries.
|
|
8
|
+
* A caller that translates a bare text in a project (the MCP translate tool
|
|
9
|
+
* with project_dir) needs to know when that text exists there ONLY with a
|
|
10
|
+
* context: a context-free cache entry for it is never read by sync, and only
|
|
11
|
+
* shadows the context-keyed ones (Round 13, Django persona).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { discoverLocaleLayout, loadSourceUnits } from './locale-layout.js';
|
|
15
|
+
import { CONTEXT_SEPARATOR } from './tm-evict.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Every source text of the project, with whether it has an entry without a
|
|
19
|
+
* context and the contexts it has.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} config - A resolved config (lib/config.js resolveConfig)
|
|
22
|
+
* @param {{ cwd: string }} options
|
|
23
|
+
* @returns {Map<string, { plain: boolean, contexts: string[] }>} source text → its entries
|
|
24
|
+
*/
|
|
25
|
+
export function sourceTextContexts(config, { cwd }) {
|
|
26
|
+
const layout = discoverLocaleLayout(config, { cwd });
|
|
27
|
+
const out = new Map();
|
|
28
|
+
for (const unit of loadSourceUnits(layout)) {
|
|
29
|
+
for (const [key, value] of Object.entries(unit.flat)) {
|
|
30
|
+
if (typeof value !== 'string') continue;
|
|
31
|
+
if (!out.has(value)) out.set(value, { plain: false, contexts: [] });
|
|
32
|
+
const entry = out.get(value);
|
|
33
|
+
const at = key.indexOf(CONTEXT_SEPARATOR);
|
|
34
|
+
if (at < 0) entry.plain = true;
|
|
35
|
+
else if (!entry.contexts.includes(key.slice(0, at))) entry.contexts.push(key.slice(0, at));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
package/lib/submit.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* submit — declarative model behind `champollion submit`.
|
|
2
|
+
* submit — declarative model behind `champollion network submit`.
|
|
3
3
|
*
|
|
4
4
|
* The CLI front door to Champollion's REVIEW-GATED community submission path.
|
|
5
5
|
* It gathers the fields a submission needs and hands the contributor a
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* @see .github/ISSUE_TEMPLATE/
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import { parseLanguagePair, formatLanguagePair } from './language-pair.js';
|
|
26
|
+
|
|
25
27
|
/**
|
|
26
28
|
* The compliance attestation every submission must carry. A single source of
|
|
27
29
|
* truth so the CLI, the issue forms, and the docs all say the SAME thing.
|
|
@@ -30,6 +32,14 @@ export const ATTESTATION_TEXT =
|
|
|
30
32
|
'I confirm this is publicly listable, contains NO corpus content or personal data, ' +
|
|
31
33
|
"and respects the source's license and any community/sovereignty restrictions.";
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The label of every "language pair(s)" field. Three types asked for pairs in
|
|
37
|
+
* two spellings (eng-crk in one, source→target in two); the reader below
|
|
38
|
+
* takes any spelling lib/language-pair.js accepts and the submission carries
|
|
39
|
+
* each pair as source>target, the form the leaderboard writes.
|
|
40
|
+
*/
|
|
41
|
+
export const PAIRS_LABEL = 'Language pair(s), ISO 639-3, one per line, source>target (e.g. eng>crk; eng-crk works too)';
|
|
42
|
+
|
|
33
43
|
/** Repo the issue forms live in. Overridable, but this is the real default. */
|
|
34
44
|
export const DEFAULT_REPO_BASE = 'https://github.com/gamedaysuits/Champollion';
|
|
35
45
|
|
|
@@ -37,6 +47,7 @@ export const DEFAULT_REPO_BASE = 'https://github.com/gamedaysuits/Champollion';
|
|
|
37
47
|
// input — single-line free text
|
|
38
48
|
// text — multi-line free text (textarea)
|
|
39
49
|
// choice — one-of a fixed list (`choices`)
|
|
50
|
+
// (a text field with `pairs: true` holds language pairs, one per line — readPairLines)
|
|
40
51
|
// bool — a required confirmation (attestation / consent); never sent in the
|
|
41
52
|
// pre-fill URL (GitHub can't pre-check a checkbox), only recorded
|
|
42
53
|
// locally and enforced by validation.
|
|
@@ -57,7 +68,7 @@ export const SUBMISSION_TYPES = [
|
|
|
57
68
|
blurb: 'An evaluation corpus or benchmark — metadata + a fetch-from-source pointer only, never the corpus content.',
|
|
58
69
|
fields: [
|
|
59
70
|
{ id: 'dataset-name', label: 'Dataset name', kind: 'input', required: true, primary: true },
|
|
60
|
-
{ id: 'pairs', label:
|
|
71
|
+
{ id: 'pairs', label: PAIRS_LABEL, kind: 'text', pairs: true, required: true },
|
|
61
72
|
{ id: 'license', label: 'License', kind: 'choice', required: true,
|
|
62
73
|
choices: ['CC-BY-4.0', 'CC-BY-SA-4.0', 'CC0-1.0', 'CC-BY-NC-4.0', 'Proprietary / all rights reserved', 'Other / unsure'] },
|
|
63
74
|
{ id: 'license-url', label: 'License URL', kind: 'input' },
|
|
@@ -135,7 +146,7 @@ export const SUBMISSION_TYPES = [
|
|
|
135
146
|
{ id: 'display-name', label: 'Provider display name (consented public name)', kind: 'input', required: true, primary: true },
|
|
136
147
|
{ id: 'provider-type', label: 'Provider type', kind: 'choice', required: true,
|
|
137
148
|
choices: ['community_org', 'agency', 'individual'] },
|
|
138
|
-
{ id: 'pairs', label:
|
|
149
|
+
{ id: 'pairs', label: PAIRS_LABEL, kind: 'text', pairs: true, required: true },
|
|
139
150
|
{ id: 'variety', label: 'Variety / script (e.g. SRO, Latn)', kind: 'input' },
|
|
140
151
|
{ id: 'dispatch-channel', label: 'Dispatch channel (the address itself is PII — omit it)', kind: 'choice',
|
|
141
152
|
choices: ['email', 'webhook', 'api'] },
|
|
@@ -158,7 +169,7 @@ export const SUBMISSION_TYPES = [
|
|
|
158
169
|
blurb: 'A published result from another system/paper — cited, never re-hosted or re-ranked as our own measurement.',
|
|
159
170
|
fields: [
|
|
160
171
|
{ id: 'system-name', label: 'System / method name', kind: 'input', required: true, primary: true },
|
|
161
|
-
{ id: 'pairs', label:
|
|
172
|
+
{ id: 'pairs', label: PAIRS_LABEL, kind: 'text', pairs: true, required: true },
|
|
162
173
|
{ id: 'dataset', label: 'Dataset / benchmark', kind: 'input', required: true },
|
|
163
174
|
{ id: 'metric', label: 'Metric', kind: 'choice', required: true,
|
|
164
175
|
choices: ['chrF++', 'chrF', 'BLEU', 'COMET', 'TER', 'Other'] },
|
|
@@ -252,7 +263,7 @@ export function repoBaseFromRepository(repoUrl) {
|
|
|
252
263
|
*/
|
|
253
264
|
export function validateSubmission(type, values = {}) {
|
|
254
265
|
const errors = [];
|
|
255
|
-
if (!type) return { ok: false, errors: ['unknown submission type — run `champollion submit --list`'] };
|
|
266
|
+
if (!type) return { ok: false, errors: ['unknown submission type — run `champollion network submit --list`'] };
|
|
256
267
|
for (const f of type.fields) {
|
|
257
268
|
const v = values[f.id];
|
|
258
269
|
if (f.kind === 'bool') {
|
|
@@ -266,10 +277,51 @@ export function validateSubmission(type, values = {}) {
|
|
|
266
277
|
const empty = v == null || String(v).trim() === '';
|
|
267
278
|
if (empty) errors.push(`missing required field: ${f.id} (${f.label})`);
|
|
268
279
|
}
|
|
280
|
+
if (f.pairs && v != null && String(v).trim() !== '') {
|
|
281
|
+
for (const e of readPairLines(v).errors) errors.push(`${f.id}: ${e}`);
|
|
282
|
+
}
|
|
269
283
|
}
|
|
270
284
|
return { ok: errors.length === 0, errors };
|
|
271
285
|
}
|
|
272
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Read a "language pair(s)" field: one pair per line, each in any spelling
|
|
289
|
+
* lib/language-pair.js accepts. Blank lines are skipped.
|
|
290
|
+
*
|
|
291
|
+
* @param {*} value
|
|
292
|
+
* @returns {{pairs: string[], errors: string[]}} pairs written source>target
|
|
293
|
+
*/
|
|
294
|
+
export function readPairLines(value) {
|
|
295
|
+
const pairs = [];
|
|
296
|
+
const errors = [];
|
|
297
|
+
String(value).split(/\r?\n/).forEach((line, i) => {
|
|
298
|
+
if (!line.trim()) return;
|
|
299
|
+
const p = parseLanguagePair(line, { label: `line ${i + 1}:` });
|
|
300
|
+
if (p.ok) pairs.push(formatLanguagePair(p));
|
|
301
|
+
else errors.push(p.error);
|
|
302
|
+
});
|
|
303
|
+
return { pairs, errors };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The values with every pair field written the one way (source>target, one
|
|
308
|
+
* per line), for the issue URL and the local record. A field that does not
|
|
309
|
+
* read cleanly is left as typed — validateSubmission reports it.
|
|
310
|
+
*
|
|
311
|
+
* @param {object} type one of SUBMISSION_TYPES
|
|
312
|
+
* @param {Record<string,*>} values
|
|
313
|
+
* @returns {Record<string,*>} a new object; `values` is not changed
|
|
314
|
+
*/
|
|
315
|
+
export function normalizeSubmissionValues(type, values = {}) {
|
|
316
|
+
const out = { ...values };
|
|
317
|
+
for (const f of type.fields) {
|
|
318
|
+
if (!f.pairs || out[f.id] == null || String(out[f.id]).trim() === '') continue;
|
|
319
|
+
const r = readPairLines(out[f.id]);
|
|
320
|
+
if (r.errors.length === 0) out[f.id] = r.pairs.join('\n');
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
|
|
273
325
|
/** The field whose value seeds the issue title (the `primary` one). */
|
|
274
326
|
function primaryField(type) {
|
|
275
327
|
return type.fields.find((f) => f.primary) || null;
|