champollion 0.3.3
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/LICENSE +133 -0
- package/README.md +387 -0
- package/bin/cli.js +278 -0
- package/index.js +135 -0
- package/lib/api-key.js +127 -0
- package/lib/autofix.js +432 -0
- package/lib/bridge/method_bridge.py +430 -0
- package/lib/card-source-resolution.mjs +284 -0
- package/lib/cards/cache.js +169 -0
- package/lib/cards/env.js +82 -0
- package/lib/cards/fetch-card-child.js +38 -0
- package/lib/cards/reader.js +435 -0
- package/lib/cards/refresh.js +111 -0
- package/lib/cards/remote.js +387 -0
- package/lib/cldf-export.mjs +540 -0
- package/lib/cldf-terms.mjs +62 -0
- package/lib/command-help.js +790 -0
- package/lib/commands/audit.js +49 -0
- package/lib/commands/card.js +454 -0
- package/lib/commands/doctor.js +559 -0
- package/lib/commands/fonts.js +489 -0
- package/lib/commands/help.js +91 -0
- package/lib/commands/init.js +1259 -0
- package/lib/commands/integrity.js +148 -0
- package/lib/commands/leaderboard.js +478 -0
- package/lib/commands/lint.js +30 -0
- package/lib/commands/models.js +177 -0
- package/lib/commands/plugin.js +103 -0
- package/lib/commands/provenance.js +45 -0
- package/lib/commands/recommend.js +75 -0
- package/lib/commands/register-corpus.js +678 -0
- package/lib/commands/repair-script.js +42 -0
- package/lib/commands/seal-corpus.js +355 -0
- package/lib/commands/seo.js +72 -0
- package/lib/commands/serve.js +147 -0
- package/lib/commands/status.js +265 -0
- package/lib/commands/submit.js +332 -0
- package/lib/commands/sync.js +89 -0
- package/lib/commands/tm.js +573 -0
- package/lib/commands/verify.js +39 -0
- package/lib/commands/watch.js +20 -0
- package/lib/commands/wrap.js +138 -0
- package/lib/commands/xliff.js +327 -0
- package/lib/commercial-eligibility.js +235 -0
- package/lib/concurrent.js +87 -0
- package/lib/config.js +523 -0
- package/lib/contamination-lane.js +76 -0
- package/lib/content-sync.js +731 -0
- package/lib/content.js +733 -0
- package/lib/corpus-registration.mjs +608 -0
- package/lib/cost-report.js +346 -0
- package/lib/diff.js +155 -0
- package/lib/docusaurus-sync.js +1256 -0
- package/lib/flatten.js +55 -0
- package/lib/format.js +954 -0
- package/lib/hash.js +159 -0
- package/lib/icu.js +473 -0
- package/lib/integrity.js +689 -0
- package/lib/license-gate.mjs +478 -0
- package/lib/license-identify.mjs +229 -0
- package/lib/lint.js +629 -0
- package/lib/method-manifest.js +60 -0
- package/lib/methods/anthropic.js +140 -0
- package/lib/methods/apertium.js +163 -0
- package/lib/methods/api.js +316 -0
- package/lib/methods/base.js +184 -0
- package/lib/methods/content-separator.js +45 -0
- package/lib/methods/deepl.js +426 -0
- package/lib/methods/direct-llm.js +586 -0
- package/lib/methods/external.js +332 -0
- package/lib/methods/fetch-with-retry.js +124 -0
- package/lib/methods/gemini.js +147 -0
- package/lib/methods/google-translate.js +402 -0
- package/lib/methods/http-utils.js +122 -0
- package/lib/methods/libretranslate.js +314 -0
- package/lib/methods/llm-coached.js +670 -0
- package/lib/methods/llm.js +592 -0
- package/lib/methods/local.js +76 -0
- package/lib/methods/microsoft-translator.js +331 -0
- package/lib/methods/openai.js +131 -0
- package/lib/methods/openrouter-client.js +327 -0
- package/lib/methods/openrouter-pricing.js +156 -0
- package/lib/methods/provider-env.js +115 -0
- package/lib/methods/provider-pricing.js +310 -0
- package/lib/methods/tilde.js +150 -0
- package/lib/methods/translated.js +229 -0
- package/lib/methods/translation-error.js +80 -0
- package/lib/models.js +258 -0
- package/lib/no-translate.js +233 -0
- package/lib/output.js +238 -0
- package/lib/pairs.js +547 -0
- package/lib/plugins.js +447 -0
- package/lib/provenance.js +323 -0
- package/lib/recommend.js +648 -0
- package/lib/registers.js +1185 -0
- package/lib/repair-script.js +266 -0
- package/lib/scripts.js +994 -0
- package/lib/seal.mjs +464 -0
- package/lib/sealed-qualifier.mjs +211 -0
- package/lib/security.js +59 -0
- package/lib/segment.js +369 -0
- package/lib/seo.js +275 -0
- package/lib/serve.js +854 -0
- package/lib/string-classify.js +85 -0
- package/lib/submit.mjs +344 -0
- package/lib/sync.js +969 -0
- package/lib/tags/bcp47.js +202 -0
- package/lib/tags/resolve.js +314 -0
- package/lib/terminology.js +111 -0
- package/lib/tm-seed.js +294 -0
- package/lib/tm.js +515 -0
- package/lib/translate-pair.js +197 -0
- package/lib/translate.js +203 -0
- package/lib/types.js +230 -0
- package/lib/validate.js +510 -0
- package/lib/verify.js +451 -0
- package/lib/watch.js +145 -0
- package/lib/xliff.js +184 -0
- package/package.json +93 -0
- package/shared/ATTRIBUTION.md +145 -0
- package/shared/CORPORA-CARDS.md +288 -0
- package/shared/DATA-SOVEREIGNTY.md +500 -0
- package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
- package/shared/card-lint-baseline.json +3189 -0
- package/shared/cards-fallback.json +1 -0
- package/shared/catalogue/card-config.json +6091 -0
- package/shared/catalogue/external-results.json +3888 -0
- package/shared/catalogue/gender-guidance.json +1038 -0
- package/shared/catalogue/method-coverage.json +1751 -0
- package/shared/catalogue/metric-coverage.json +170 -0
- package/shared/catalogue/metric-reliability.json +1 -0
- package/shared/catalogue/register-presets.json +3180 -0
- package/shared/catalogue/vitality-scales.json +55 -0
- package/shared/cldr-index.json +1115 -0
- package/shared/code-bridge.json +253 -0
- package/shared/corpora-cards-v1-reference.md +281 -0
- package/shared/curated-dictionary-flags.json +35 -0
- package/shared/curated-endonyms.json +35 -0
- package/shared/curated-fsts.json +51 -0
- package/shared/curated-orthography-conventions.json +26 -0
- package/shared/curated-sil-resources.json +374 -0
- package/shared/curated-tools.json +41 -0
- package/shared/docent/corpus.json +11333 -0
- package/shared/docent/faq.en.json +564 -0
- package/shared/docent/register-blocks.json +60 -0
- package/shared/docent/system-prompt.md +144 -0
- package/shared/domain-taxonomy.json +35 -0
- package/shared/explainers/glossary.json +2975 -0
- package/shared/explainers/tc-features.json +20112 -0
- package/shared/explainers/term-watchlist.json +147 -0
- package/shared/human-services.json +59 -0
- package/shared/license-corrections.json +261 -0
- package/shared/license-evidence.json +13452 -0
- package/shared/licenses.json +6781 -0
- package/shared/method-registry.json +236 -0
- package/shared/metric-registry.json +620 -0
- package/shared/model-aliases.json +7 -0
- package/shared/schemas/champollion-plugin.schema.json +206 -0
- package/shared/schemas/corpora-card.schema.json +957 -0
- package/shared/schemas/domain-taxonomy.schema.json +64 -0
- package/shared/schemas/external-results.schema.json +314 -0
- package/shared/schemas/human-services.schema.json +90 -0
- package/shared/schemas/language-card.schema.json +1308 -0
- package/shared/schemas/licenses.schema.json +155 -0
- package/shared/schemas/method-card.schema.json +412 -0
- package/shared/schemas/method-registry.schema.json +85 -0
- package/shared/schemas/metric-registry.schema.json +96 -0
- package/shared/schemas/metric-reliability.schema.json +178 -0
- package/shared/schemas/model-aliases.schema.json +27 -0
- package/shared/schemas/source-snapshot.schema.json +96 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* translation-error.js — shared recorder for the most recent translation
|
|
3
|
+
* HTTP failure, used to give accurate setup help.
|
|
4
|
+
*
|
|
5
|
+
* WHY: When a sync fails, getSetupHelp() previously always said "check your
|
|
6
|
+
* dashboard for quota/billing" — wrong and misleading when the real problem
|
|
7
|
+
* was a bad/expired key (HTTP 401/403). The user burns time checking billing
|
|
8
|
+
* when they should be re-checking the key value.
|
|
9
|
+
*
|
|
10
|
+
* The HTTP clients (openrouter-client, direct-llm, fetch-with-retry) record
|
|
11
|
+
* the status of a failing request here as they give up on it. getSetupHelp()
|
|
12
|
+
* then reads the last-seen failure and tailors its guidance:
|
|
13
|
+
* - auth (401/403): the key itself is the problem, not billing
|
|
14
|
+
* - quota (429): rate-limited / over quota → check billing/quota
|
|
15
|
+
* - server (5xx): upstream problem → wait and retry
|
|
16
|
+
*
|
|
17
|
+
* The method instance that ran the translation is discarded before
|
|
18
|
+
* getSetupHelp() is called (sync.js builds a fresh instance), so per-instance
|
|
19
|
+
* state can't carry the status across. A module-level "last error" is the
|
|
20
|
+
* pragmatic carrier: failures are almost always systemic (one bad key fails
|
|
21
|
+
* every pair), so the last-recorded status is representative of the failure
|
|
22
|
+
* the user is about to read help for.
|
|
23
|
+
*
|
|
24
|
+
* Node is single-threaded, so the assignment itself never races. Callers that
|
|
25
|
+
* want a clean slate (e.g. the start of a sync run) call reset first.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
let _last = null; // { status: number|null, kind: string }
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Classify an HTTP status into a coarse failure kind.
|
|
32
|
+
*
|
|
33
|
+
* @param {number} status - HTTP status code
|
|
34
|
+
* @returns {'auth'|'quota'|'server'|'unknown'}
|
|
35
|
+
*/
|
|
36
|
+
function classifyTranslationError(status) {
|
|
37
|
+
if (typeof status !== 'number') return 'unknown';
|
|
38
|
+
if (status === 401 || status === 403) return 'auth';
|
|
39
|
+
if (status === 429) return 'quota';
|
|
40
|
+
if (status >= 500 && status <= 599) return 'server';
|
|
41
|
+
return 'unknown';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Record a failing HTTP status. No-ops for non-numbers and for 2xx
|
|
46
|
+
* (a successful response is not a failure to report).
|
|
47
|
+
*
|
|
48
|
+
* @param {number} status - HTTP status code of the failed request
|
|
49
|
+
* @returns {{ status: number|null, kind: string }|null} the recorded entry
|
|
50
|
+
*/
|
|
51
|
+
function recordTranslationError(status) {
|
|
52
|
+
if (typeof status !== 'number' || (status >= 200 && status < 300)) return _last;
|
|
53
|
+
_last = { status, kind: classifyTranslationError(status) };
|
|
54
|
+
return _last;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the most recently recorded translation failure, or null if none.
|
|
59
|
+
*
|
|
60
|
+
* @returns {{ status: number|null, kind: string }|null}
|
|
61
|
+
*/
|
|
62
|
+
function getLastTranslationError() {
|
|
63
|
+
return _last;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Clear the recorded failure. Call at the start of a fresh sync run so a
|
|
68
|
+
* stale error from a prior in-process sync (e.g. watch mode) can't leak
|
|
69
|
+
* into the next run's help text.
|
|
70
|
+
*/
|
|
71
|
+
function resetTranslationError() {
|
|
72
|
+
_last = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
classifyTranslationError,
|
|
77
|
+
recordTranslationError,
|
|
78
|
+
getLastTranslationError,
|
|
79
|
+
resetTranslationError,
|
|
80
|
+
};
|
package/lib/models.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model listing service — fetches available models from provider APIs.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS:
|
|
5
|
+
* The init wizard, `champollion models` command, and method-level validation
|
|
6
|
+
* all need the same thing: "what models does this provider offer for my
|
|
7
|
+
* API key?" Previously, each method class had its own _fetchModels()
|
|
8
|
+
* with duplicated fetch logic. This module centralizes it.
|
|
9
|
+
*
|
|
10
|
+
* DESIGN:
|
|
11
|
+
* - Each provider entry defines how to call its model list API and
|
|
12
|
+
* how to filter the results to chat-capable models.
|
|
13
|
+
* - Results are cached per-process to avoid redundant API calls.
|
|
14
|
+
* - Returns null on failure (network, invalid key) — callers decide
|
|
15
|
+
* how to handle (fallback prompt, skip, etc.).
|
|
16
|
+
*
|
|
17
|
+
* PROVIDER API ENDPOINTS:
|
|
18
|
+
* - Gemini: GET https://generativelanguage.googleapis.com/v1beta/models?key=...
|
|
19
|
+
* - OpenAI: GET https://api.openai.com/v1/models (Bearer token)
|
|
20
|
+
* - Anthropic: GET https://api.anthropic.com/v1/models (x-api-key header)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
import { getEnvOrFileVar } from './api-key.js';
|
|
27
|
+
|
|
28
|
+
// Per-process cache: provider name → model ID array (or null if fetch failed)
|
|
29
|
+
const _modelCache = new Map();
|
|
30
|
+
|
|
31
|
+
// Lazy-loaded alias map (loaded once from shared/model-aliases.json)
|
|
32
|
+
let _aliasCache = null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load the model alias map from shared/model-aliases.json.
|
|
36
|
+
*
|
|
37
|
+
* Resolves the path relative to the monorepo root (two levels up from cli/lib/).
|
|
38
|
+
* Returns an empty object if the file doesn't exist or fails to parse,
|
|
39
|
+
* so callers can always safely check `aliases[name]`.
|
|
40
|
+
*
|
|
41
|
+
* @returns {Object<string, string>} Short name → full OpenRouter slug
|
|
42
|
+
*/
|
|
43
|
+
function _loadAliases() {
|
|
44
|
+
if (_aliasCache) return _aliasCache;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
// Prefer the package-bundled copy (cli/shared/, shipped via the prepack
|
|
49
|
+
// build-cards-fallback.mjs), then fall back to the monorepo-root SSOT for
|
|
50
|
+
// in-repo dev. Without the package copy, an installed CLI found no alias map
|
|
51
|
+
// and `--model <alias>` silently failed to resolve.
|
|
52
|
+
const aliasPaths = [
|
|
53
|
+
path.resolve(__dirname, '..', 'shared', 'model-aliases.json'),
|
|
54
|
+
path.resolve(__dirname, '..', '..', 'shared', 'model-aliases.json'),
|
|
55
|
+
];
|
|
56
|
+
let raw = null;
|
|
57
|
+
for (const p of aliasPaths) {
|
|
58
|
+
try { raw = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ }
|
|
59
|
+
}
|
|
60
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
61
|
+
// Strip metadata keys (e.g., _comment) — only keep actual aliases
|
|
62
|
+
_aliasCache = {};
|
|
63
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
64
|
+
if (!key.startsWith('_') && typeof value === 'string') {
|
|
65
|
+
_aliasCache[key] = value;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
_aliasCache = {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return _aliasCache;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a model name or alias to a full OpenRouter slug.
|
|
77
|
+
*
|
|
78
|
+
* Resolution order:
|
|
79
|
+
* 1. If contains '/' → already a full slug, pass through
|
|
80
|
+
* 2. Check shared/model-aliases.json for a matching alias
|
|
81
|
+
* 3. Pass through as-is (user may know what they're doing)
|
|
82
|
+
*
|
|
83
|
+
* @param {string} nameOrSlug - User-provided model identifier
|
|
84
|
+
* @returns {string} Full OpenRouter model slug
|
|
85
|
+
*/
|
|
86
|
+
function resolveModel(nameOrSlug) {
|
|
87
|
+
if (!nameOrSlug || typeof nameOrSlug !== 'string') return nameOrSlug;
|
|
88
|
+
|
|
89
|
+
// Already a full slug (contains provider prefix)
|
|
90
|
+
if (nameOrSlug.includes('/')) return nameOrSlug;
|
|
91
|
+
|
|
92
|
+
// Check aliases
|
|
93
|
+
const aliases = _loadAliases();
|
|
94
|
+
if (aliases[nameOrSlug]) return aliases[nameOrSlug];
|
|
95
|
+
|
|
96
|
+
// Pass through — could be a provider-specific model name (e.g., gemini-2.0-flash)
|
|
97
|
+
return nameOrSlug;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Provider configurations — how to fetch and filter models for each provider.
|
|
102
|
+
*
|
|
103
|
+
* Adding a new provider:
|
|
104
|
+
* 1. Add an entry here with fetch + filter functions
|
|
105
|
+
* 2. The method class's _fetchModels() delegates to fetchAvailableModels()
|
|
106
|
+
* 3. The init wizard and `models` command automatically pick it up
|
|
107
|
+
*/
|
|
108
|
+
const PROVIDERS = {
|
|
109
|
+
gemini: {
|
|
110
|
+
envVar: 'GEMINI_API_KEY',
|
|
111
|
+
label: 'Google Gemini',
|
|
112
|
+
async fetch(apiKey) {
|
|
113
|
+
const response = await fetch(
|
|
114
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`
|
|
115
|
+
);
|
|
116
|
+
if (!response.ok) return null;
|
|
117
|
+
const data = await response.json();
|
|
118
|
+
// Only include models that support generateContent (not embeddings-only).
|
|
119
|
+
// Strip the "models/" prefix that Gemini returns.
|
|
120
|
+
return (data.models || [])
|
|
121
|
+
.filter(m => m.supportedGenerationMethods?.includes('generateContent'))
|
|
122
|
+
.map(m => m.name.replace('models/', ''));
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
openai: {
|
|
127
|
+
envVar: 'OPENAI_API_KEY',
|
|
128
|
+
label: 'OpenAI',
|
|
129
|
+
async fetch(apiKey) {
|
|
130
|
+
const response = await fetch('https://api.openai.com/v1/models', {
|
|
131
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
132
|
+
});
|
|
133
|
+
if (!response.ok) return null;
|
|
134
|
+
const data = await response.json();
|
|
135
|
+
// Filter to chat-capable models — skip embeddings, whisper, dall-e, tts, etc.
|
|
136
|
+
return (data.data || [])
|
|
137
|
+
.map(m => m.id)
|
|
138
|
+
.filter(id =>
|
|
139
|
+
id.startsWith('gpt-') ||
|
|
140
|
+
id.startsWith('o1') ||
|
|
141
|
+
id.startsWith('o3') ||
|
|
142
|
+
id.startsWith('o4') ||
|
|
143
|
+
id.startsWith('chatgpt-')
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
anthropic: {
|
|
149
|
+
envVar: 'ANTHROPIC_API_KEY',
|
|
150
|
+
label: 'Anthropic',
|
|
151
|
+
async fetch(apiKey) {
|
|
152
|
+
const response = await fetch('https://api.anthropic.com/v1/models', {
|
|
153
|
+
headers: {
|
|
154
|
+
'x-api-key': apiKey,
|
|
155
|
+
'anthropic-version': '2023-06-01',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
if (!response.ok) return null;
|
|
159
|
+
const data = await response.json();
|
|
160
|
+
return (data.data || []).map(m => m.id);
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Fetch available chat-capable models for a provider.
|
|
167
|
+
*
|
|
168
|
+
* Uses the provider's real API to get the actual model list the user's
|
|
169
|
+
* API key has access to. Results are cached per-process.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} provider - Provider name: 'gemini', 'openai', 'anthropic'
|
|
172
|
+
* @param {string} apiKey - The provider's API key
|
|
173
|
+
* @returns {Promise<string[]|null>} Array of model IDs, or null on failure
|
|
174
|
+
*/
|
|
175
|
+
async function fetchAvailableModels(provider, apiKey) {
|
|
176
|
+
if (!apiKey) return null;
|
|
177
|
+
|
|
178
|
+
const config = PROVIDERS[provider];
|
|
179
|
+
if (!config) return null;
|
|
180
|
+
|
|
181
|
+
// Check cache — undefined = never tried, null = tried and failed
|
|
182
|
+
const cached = _modelCache.get(provider);
|
|
183
|
+
if (cached !== undefined) return cached;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const models = await config.fetch(apiKey);
|
|
187
|
+
if (models && models.length > 0) {
|
|
188
|
+
_modelCache.set(provider, models);
|
|
189
|
+
return models;
|
|
190
|
+
}
|
|
191
|
+
_modelCache.set(provider, null);
|
|
192
|
+
return null;
|
|
193
|
+
} catch {
|
|
194
|
+
_modelCache.set(provider, null);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Resolve an API key for a provider from environment or .env files.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} provider - Provider name
|
|
203
|
+
* @param {string} [cwd] - Working directory for .env file lookup
|
|
204
|
+
* @returns {string|null} API key or null
|
|
205
|
+
*/
|
|
206
|
+
function resolveProviderApiKey(provider, cwd) {
|
|
207
|
+
const config = PROVIDERS[provider];
|
|
208
|
+
if (!config) return null;
|
|
209
|
+
return getEnvOrFileVar(config.envVar) || getEnvOrFileVar(config.envVar, cwd);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get the display label for a provider.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} provider - Provider name
|
|
216
|
+
* @returns {string} Human-readable label
|
|
217
|
+
*/
|
|
218
|
+
function getProviderLabel(provider) {
|
|
219
|
+
return PROVIDERS[provider]?.label || provider;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Check if a provider has model listing support.
|
|
224
|
+
*
|
|
225
|
+
* @param {string} provider - Provider name
|
|
226
|
+
* @returns {boolean}
|
|
227
|
+
*/
|
|
228
|
+
function isListableProvider(provider) {
|
|
229
|
+
return provider in PROVIDERS;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get all provider names that support model listing.
|
|
234
|
+
*
|
|
235
|
+
* @returns {string[]}
|
|
236
|
+
*/
|
|
237
|
+
function getListableProviders() {
|
|
238
|
+
return Object.keys(PROVIDERS);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Clear the per-process model cache.
|
|
243
|
+
* Primarily for testing — allows re-fetching in a long-running process.
|
|
244
|
+
*/
|
|
245
|
+
function clearModelCache() {
|
|
246
|
+
_modelCache.clear();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export {
|
|
250
|
+
fetchAvailableModels,
|
|
251
|
+
resolveModel,
|
|
252
|
+
resolveProviderApiKey,
|
|
253
|
+
getProviderLabel,
|
|
254
|
+
isListableProvider,
|
|
255
|
+
getListableProviders,
|
|
256
|
+
clearModelCache,
|
|
257
|
+
PROVIDERS,
|
|
258
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* no-translate.js — keys whose correct translation is the source, verbatim.
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM THIS SOLVES:
|
|
5
|
+
* Some values have exactly one correct rendering in every locale: a URL,
|
|
6
|
+
* a repo path, a package name. The post-translation quality gate rejects
|
|
7
|
+
* source-echo (lib/validate.js check 2), so for these keys the CORRECT
|
|
8
|
+
* answer always FAILS. That has two observed failure modes, both real,
|
|
9
|
+
* both seen in production:
|
|
10
|
+
*
|
|
11
|
+
* 1. Weak models learn to defeat the gate by bending the value just
|
|
12
|
+
* enough to stop being an echo. Observed on a live site: 48 corrupted
|
|
13
|
+
* URLs across 13 locales — fabricated fragments (".../view/1954#fr"),
|
|
14
|
+
* stray trailing "#" and "/", a U+200E LEFT-TO-RIGHT MARK prepended
|
|
15
|
+
* in Arabic, a U+200B ZERO WIDTH SPACE appended in Hindi. The
|
|
16
|
+
* invisible-character ones break the link outright.
|
|
17
|
+
* 2. Strong models return the value unchanged, correctly, and fail the
|
|
18
|
+
* gate — so `champollion sync` exits non-zero forever. A pre-commit
|
|
19
|
+
* hook wired to sync can then never be satisfied, and the only way
|
|
20
|
+
* past is to disable the whole gate.
|
|
21
|
+
*
|
|
22
|
+
* There is no threshold that fixes this, because the gate is asking the
|
|
23
|
+
* wrong question. The fix is to declare the key out of scope: never send
|
|
24
|
+
* it to a backend, never gate it, never bill it — copy it verbatim.
|
|
25
|
+
*
|
|
26
|
+
* TWO WAYS A KEY BECOMES NO-TRANSLATE:
|
|
27
|
+
* 1. `noTranslate` config patterns — dot-path keys and/or globs:
|
|
28
|
+
* "noTranslate": ["**.url", "pages.software.*.repo", "meta.appId"]
|
|
29
|
+
* 2. Auto-detected bare URLs — a source value that is nothing but a
|
|
30
|
+
* `scheme://…` URL. On by default (`noTranslateUrls`), because the
|
|
31
|
+
* current behaviour has no correct outcome. Opt out with
|
|
32
|
+
* `"noTranslateUrls": false`.
|
|
33
|
+
*
|
|
34
|
+
* WHERE IT PLUGS IN: `diffLocale` (lib/diff.js) takes the resulting matcher
|
|
35
|
+
* and routes matching keys into a `noTranslate` bucket instead of
|
|
36
|
+
* `toProcess`. Because the cost estimator diffs with the same matcher, the
|
|
37
|
+
* keys are excluded from the bill by construction rather than by a second
|
|
38
|
+
* rule that could drift.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A value is "a bare URL" when the whole thing, trimmed, is one absolute
|
|
43
|
+
* URL and nothing else.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately NOT a substring match: "Read the paper at https://…" is
|
|
46
|
+
* prose with a URL in it and must still be translated. Only a value that
|
|
47
|
+
* IS the URL has no translatable content.
|
|
48
|
+
*
|
|
49
|
+
* Scheme grammar follows RFC 3986 (ALPHA *( ALPHA / DIGIT / "+" / "-" / "."))
|
|
50
|
+
* and requires the `://` authority form, so `https://`, `ftp://` and
|
|
51
|
+
* `ipfs://` match while `mailto:` and a bare `example.com` do not.
|
|
52
|
+
*/
|
|
53
|
+
const BARE_URL = /^[a-z][a-z0-9+.-]*:\/\/\S+$/i;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Is this source value a bare URL?
|
|
57
|
+
*
|
|
58
|
+
* @param {unknown} value - Source value to test
|
|
59
|
+
* @returns {boolean} True when the trimmed value is exactly one absolute URL
|
|
60
|
+
*/
|
|
61
|
+
function isBareUrl(value) {
|
|
62
|
+
return typeof value === 'string' && BARE_URL.test(value.trim());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compile one dot-path pattern into a segment matcher.
|
|
67
|
+
*
|
|
68
|
+
* Pattern grammar (segments split on `.`):
|
|
69
|
+
* - a literal segment matches that segment exactly
|
|
70
|
+
* - `*` matches any characters WITHIN one segment (`page*` → `pageTitle`)
|
|
71
|
+
* - `**` matches zero or more whole segments (`**.url` matches `a.b.url`
|
|
72
|
+
* and a top-level `url`)
|
|
73
|
+
* - a pattern with no wildcard is an exact dot-path
|
|
74
|
+
*
|
|
75
|
+
* @param {string} pattern - e.g. '**.url', 'pages.software.*.repo'
|
|
76
|
+
* @returns {(keySegments: string[]) => boolean} Segment-array matcher
|
|
77
|
+
*/
|
|
78
|
+
function compilePattern(pattern) {
|
|
79
|
+
const patternSegments = pattern.split('.');
|
|
80
|
+
|
|
81
|
+
// Per-segment regexes, built once. `**` is handled structurally below and
|
|
82
|
+
// never reaches this map.
|
|
83
|
+
const segmentTests = patternSegments.map(seg => {
|
|
84
|
+
if (seg === '**') return null;
|
|
85
|
+
if (!seg.includes('*')) return (s) => s === seg;
|
|
86
|
+
// Escape everything regex-significant, then turn `*` into "any run of
|
|
87
|
+
// characters that is not a segment separator".
|
|
88
|
+
const source = '^' + seg
|
|
89
|
+
.split('*')
|
|
90
|
+
.map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
91
|
+
.join('[^.]*') + '$';
|
|
92
|
+
const re = new RegExp(source);
|
|
93
|
+
return (s) => re.test(s);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Classic backtracking glob match over segment arrays. Locale key depth is
|
|
98
|
+
* small (single digits), so recursion is cheap and the code stays readable.
|
|
99
|
+
*/
|
|
100
|
+
function match(pi, ki, keySegments) {
|
|
101
|
+
if (pi === patternSegments.length) return ki === keySegments.length;
|
|
102
|
+
|
|
103
|
+
if (patternSegments[pi] === '**') {
|
|
104
|
+
// `**` consumes zero or more segments — try every split point.
|
|
105
|
+
for (let skip = ki; skip <= keySegments.length; skip++) {
|
|
106
|
+
if (match(pi + 1, skip, keySegments)) return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (ki >= keySegments.length) return false;
|
|
112
|
+
if (!segmentTests[pi](keySegments[ki])) return false;
|
|
113
|
+
return match(pi + 1, ki + 1, keySegments);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (keySegments) => match(0, 0, keySegments);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate the no-translate config fields, failing loud on anything the
|
|
121
|
+
* matcher could not honour.
|
|
122
|
+
*
|
|
123
|
+
* A misspelled or wrong-typed `noTranslate` must never degrade to "translate
|
|
124
|
+
* everything": the user wrote it precisely so certain keys would be left
|
|
125
|
+
* alone, and silently ignoring it re-opens the corruption path this module
|
|
126
|
+
* exists to close.
|
|
127
|
+
*
|
|
128
|
+
* @param {unknown} patterns - Raw config.noTranslate
|
|
129
|
+
* @param {unknown} urls - Raw config.noTranslateUrls
|
|
130
|
+
* @throws {Error} With code CHAMPOLLION_CONFIG_INVALID
|
|
131
|
+
*/
|
|
132
|
+
function validateNoTranslateConfig(patterns, urls) {
|
|
133
|
+
const fail = (message) => {
|
|
134
|
+
const e = new Error(message);
|
|
135
|
+
e.code = 'CHAMPOLLION_CONFIG_INVALID';
|
|
136
|
+
throw e;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (patterns != null) {
|
|
140
|
+
if (!Array.isArray(patterns)) {
|
|
141
|
+
fail(
|
|
142
|
+
'"noTranslate" must be an array of dot-path keys or glob patterns, '
|
|
143
|
+
+ `got ${typeof patterns}. Example: "noTranslate": ["**.url", "pages.software.*.repo"]`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
for (const p of patterns) {
|
|
147
|
+
if (typeof p !== 'string' || p.trim() === '') {
|
|
148
|
+
fail(`"noTranslate" entries must be non-empty strings — found ${JSON.stringify(p)}.`);
|
|
149
|
+
}
|
|
150
|
+
if (p.includes('..')) {
|
|
151
|
+
fail(`"noTranslate" pattern "${p}" has an empty path segment. Use "**" to match any depth.`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (urls != null && typeof urls !== 'boolean') {
|
|
157
|
+
fail(`"noTranslateUrls" must be a boolean, got ${typeof urls}. Set it to false to translate URL-valued keys.`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build the no-translate matcher for a resolved config.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} config - Resolved config (reads `noTranslate`, `noTranslateUrls`)
|
|
165
|
+
* @returns {NoTranslateMatcher}
|
|
166
|
+
*
|
|
167
|
+
* @typedef {object} NoTranslateMatcher
|
|
168
|
+
* @property {boolean} active - False when nothing is configured and URL
|
|
169
|
+
* auto-detection is off. Callers can skip the whole lane.
|
|
170
|
+
* @property {(key: string, value: unknown) => boolean} matches - Is this key
|
|
171
|
+
* exempt from translation?
|
|
172
|
+
* @property {(key: string, value: unknown) => string|null} reason - Why it is
|
|
173
|
+
* exempt ('pattern "**.url"' / 'auto-detected URL'), or null.
|
|
174
|
+
* @property {string[]} patterns - The configured patterns, for reporting.
|
|
175
|
+
* @property {boolean} urls - Whether URL auto-detection is on.
|
|
176
|
+
*/
|
|
177
|
+
function compileNoTranslate(config = {}) {
|
|
178
|
+
validateNoTranslateConfig(config.noTranslate, config.noTranslateUrls);
|
|
179
|
+
|
|
180
|
+
const patterns = Array.isArray(config.noTranslate) ? [...config.noTranslate] : [];
|
|
181
|
+
const urls = config.noTranslateUrls !== false;
|
|
182
|
+
const compiled = patterns.map(p => ({ pattern: p, test: compilePattern(p) }));
|
|
183
|
+
|
|
184
|
+
// Segment splits are pure and repeated across every locale in a run.
|
|
185
|
+
const segmentCache = new Map();
|
|
186
|
+
const segmentsOf = (key) => {
|
|
187
|
+
let segs = segmentCache.get(key);
|
|
188
|
+
if (!segs) {
|
|
189
|
+
segs = key.split('.');
|
|
190
|
+
segmentCache.set(key, segs);
|
|
191
|
+
}
|
|
192
|
+
return segs;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const reason = (key, value) => {
|
|
196
|
+
const segs = segmentsOf(key);
|
|
197
|
+
for (const { pattern, test } of compiled) {
|
|
198
|
+
if (test(segs)) return `pattern "${pattern}"`;
|
|
199
|
+
}
|
|
200
|
+
if (urls && isBareUrl(value)) return 'auto-detected URL';
|
|
201
|
+
return null;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
active: compiled.length > 0 || urls,
|
|
206
|
+
matches: (key, value) => reason(key, value) !== null,
|
|
207
|
+
reason,
|
|
208
|
+
patterns,
|
|
209
|
+
urls,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Matcher that exempts nothing — for callers with no config in hand.
|
|
215
|
+
*
|
|
216
|
+
* @type {NoTranslateMatcher}
|
|
217
|
+
*/
|
|
218
|
+
const NO_TRANSLATE_NONE = {
|
|
219
|
+
active: false,
|
|
220
|
+
matches: () => false,
|
|
221
|
+
reason: () => null,
|
|
222
|
+
patterns: [],
|
|
223
|
+
urls: false,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export {
|
|
227
|
+
compileNoTranslate,
|
|
228
|
+
compilePattern,
|
|
229
|
+
isBareUrl,
|
|
230
|
+
validateNoTranslateConfig,
|
|
231
|
+
NO_TRANSLATE_NONE,
|
|
232
|
+
BARE_URL,
|
|
233
|
+
};
|