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
package/lib/lint.js
CHANGED
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { resolveConfig } from './config.js';
|
|
9
|
-
import {
|
|
10
|
-
import { readLocaleFile, detectFormatFromDir, getExtension } from './format.js';
|
|
9
|
+
import { discoverLocaleLayout, readLocaleFlat } from './locale-layout.js';
|
|
11
10
|
import { isNonTranslatableString, isNonTranslatableLintExtended } from './string-classify.js';
|
|
12
11
|
import { output } from './output.js';
|
|
13
12
|
|
|
@@ -50,6 +49,25 @@ const FRAMEWORKS = {
|
|
|
50
49
|
frameworkImport: /from\s+['"]vue-i18n['"]/,
|
|
51
50
|
translatableAttrs: ['placeholder', 'aria-label', 'alt', 'title', 'content', 'label'],
|
|
52
51
|
},
|
|
52
|
+
// Docusaurus: copy is translated by <Translate> / translate({ id, message }).
|
|
53
|
+
// Dogfood 2026-08-28 (finding 8): ten pages of champollion.dev were
|
|
54
|
+
// written with raw JSX prose next to an i18n setup that was never used —
|
|
55
|
+
// and the lint called the site "generic", looked only for t('key'), and
|
|
56
|
+
// could not see it.
|
|
57
|
+
docusaurus: {
|
|
58
|
+
name: 'Docusaurus',
|
|
59
|
+
srcDirs: ['src'],
|
|
60
|
+
extensions: ['.tsx', '.jsx', '.ts', '.js'],
|
|
61
|
+
i18nCallPatterns: [
|
|
62
|
+
/<Translate\b[^>]*\bid\s*=\s*['"]([^'"]+)['"]/g,
|
|
63
|
+
/<Translate\b(?![^>]*\bid\s*=)[^>]*>([^<{]+)<\/Translate>/g,
|
|
64
|
+
/\btranslate\(\s*\{[^}]*?\bid\s*:\s*['"`]([^'"`]+)['"`]/g,
|
|
65
|
+
],
|
|
66
|
+
frameworkImport: /from\s+['"]@docusaurus\/Translate['"]/,
|
|
67
|
+
translatableAttrs: ['placeholder', 'aria-label', 'alt', 'title', 'label'],
|
|
68
|
+
// Text inside these tags IS translated — never flag it.
|
|
69
|
+
wrapperTags: ['Translate'],
|
|
70
|
+
},
|
|
53
71
|
hugo: {
|
|
54
72
|
name: 'Hugo',
|
|
55
73
|
srcDirs: ['layouts', 'themes'],
|
|
@@ -62,6 +80,9 @@ const FRAMEWORKS = {
|
|
|
62
80
|
},
|
|
63
81
|
};
|
|
64
82
|
|
|
83
|
+
// Tags whose text content is code, never translatable copy.
|
|
84
|
+
const CODE_TAGS = new Set(['code', 'pre', 'kbd', 'samp', 'var']);
|
|
85
|
+
|
|
65
86
|
// Default generic framework for unknown projects
|
|
66
87
|
const GENERIC_FRAMEWORK = {
|
|
67
88
|
name: 'generic',
|
|
@@ -85,6 +106,11 @@ const GENERIC_FRAMEWORK = {
|
|
|
85
106
|
* @returns {{ name: string, extensions: string[], srcDirs: string[], i18nCallPatterns: RegExp[], frameworkImport: RegExp|null }} Framework detection result
|
|
86
107
|
*/
|
|
87
108
|
function detectFramework(cwd) {
|
|
109
|
+
// Docusaurus before Hugo: its own config file is unambiguous, while a
|
|
110
|
+
// Docusaurus repo can carry a stray config.yaml.
|
|
111
|
+
const docusaurusConfigs = ['docusaurus.config.js', 'docusaurus.config.ts', 'docusaurus.config.mjs', 'docusaurus.config.cjs'];
|
|
112
|
+
if (docusaurusConfigs.some(cfg => fs.existsSync(path.join(cwd, cfg)))) return FRAMEWORKS.docusaurus;
|
|
113
|
+
|
|
88
114
|
// Check for Hugo config files first
|
|
89
115
|
const hugoConfigs = ['hugo.toml', 'hugo.yaml', 'hugo.yml', 'config.toml', 'config.yaml'];
|
|
90
116
|
for (const cfg of hugoConfigs) {
|
|
@@ -97,6 +123,7 @@ function detectFramework(cwd) {
|
|
|
97
123
|
try {
|
|
98
124
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
99
125
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
126
|
+
if (allDeps['@docusaurus/core']) return FRAMEWORKS.docusaurus;
|
|
100
127
|
if (allDeps['next-intl']) return FRAMEWORKS['next-intl'];
|
|
101
128
|
if (allDeps['react-i18next'] || allDeps['i18next']) return FRAMEWORKS['react-i18next'];
|
|
102
129
|
if (allDeps['vue-i18n']) return FRAMEWORKS['vue-i18n'];
|
|
@@ -236,6 +263,16 @@ function extractHardcodedStrings(content, filePath, framework, minLength) {
|
|
|
236
263
|
let match;
|
|
237
264
|
while ((match = textPattern.exec(line)) !== null) {
|
|
238
265
|
const text = match[1].trim();
|
|
266
|
+
// Text whose enclosing tag is a translation wrapper (<Translate>) is
|
|
267
|
+
// already translated.
|
|
268
|
+
const open = line.lastIndexOf('<', match.index);
|
|
269
|
+
const tag = open >= 0 ? (line.slice(open).match(/^<\/?([A-Za-z][\w.]*)/) || [])[1] : null;
|
|
270
|
+
if (tag && framework.wrapperTags && framework.wrapperTags.includes(tag)) continue;
|
|
271
|
+
// Code is not copy: <code>cli/shared/x.json</code>, <kbd>Ctrl</kbd>.
|
|
272
|
+
if (tag && CODE_TAGS.has(tag)) continue;
|
|
273
|
+
// No letters at all is not text — usually JS caught between a `>`
|
|
274
|
+
// comparison and a JSX tag (`n > 0 && <Badge`).
|
|
275
|
+
if (!/\p{L}/u.test(text)) continue;
|
|
239
276
|
if (shouldFlagString(text, minLength)) {
|
|
240
277
|
results.push({ line: i + 1, text, context: 'jsx-text' });
|
|
241
278
|
}
|
|
@@ -505,6 +542,16 @@ async function runLint(options = {}) {
|
|
|
505
542
|
}
|
|
506
543
|
|
|
507
544
|
if (sourceFiles.length === 0) {
|
|
545
|
+
// A lint that checked nothing is not a pass: it used to exit 0 with "No
|
|
546
|
+
// source files found to lint." while verify failed on the same project
|
|
547
|
+
// (Round 4, i18next persona) — a CI gate pointed at the wrong folder
|
|
548
|
+
// stayed green. Name where it looked and how to point it elsewhere.
|
|
549
|
+
const rel = (p) => path.relative(cwd, path.resolve(cwd, p)).split(path.sep).join('/') || '.';
|
|
550
|
+
const lookedIn = srcDir ? [rel(srcDir)] : framework.srcDirs.map(rel);
|
|
551
|
+
const exts = framework.extensions.join(', ');
|
|
552
|
+
const reason = `no ${exts} files in ${lookedIn.map(d => `${d}/`).join(', ')}`
|
|
553
|
+
+ `${srcDir ? '' : ` (the ${framework.name} defaults)`}${ignorePatterns.length > 0 ? ', after the ignore list' : ''}`;
|
|
554
|
+
const hint = 'Point it at your code: `champollion lint --src <dir>`, or "lint": { "srcDir": "<dir>" } in champollion.config.json.';
|
|
508
555
|
if (cliArgs.json) {
|
|
509
556
|
console.log(JSON.stringify({
|
|
510
557
|
command: 'lint',
|
|
@@ -512,26 +559,34 @@ async function runLint(options = {}) {
|
|
|
512
559
|
filesScanned: 0,
|
|
513
560
|
findings: [],
|
|
514
561
|
count: 0,
|
|
562
|
+
nothingChecked: true,
|
|
563
|
+
lookedIn,
|
|
564
|
+
extensions: framework.extensions,
|
|
565
|
+
error: `Nothing linted: ${reason}. ${hint}`,
|
|
515
566
|
}, null, 2));
|
|
516
|
-
return 0;
|
|
567
|
+
return warnOnly ? 0 : 1;
|
|
517
568
|
}
|
|
518
|
-
output.
|
|
519
|
-
return 0;
|
|
569
|
+
output.error(`Nothing linted: ${reason}. ${hint}`);
|
|
570
|
+
return warnOnly ? 0 : 1;
|
|
520
571
|
}
|
|
521
572
|
|
|
522
573
|
output.info(`Scanning ${sourceFiles.length} file(s)...`);
|
|
523
574
|
|
|
524
|
-
// 3. Load locale keys for cross-referencing
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
575
|
+
// 3. Load locale keys for cross-referencing — every source file of the
|
|
576
|
+
// layout (one for flat projects; every namespace file for i18next's
|
|
577
|
+
// folder-per-locale shape). Keys stay bare: code calls t('nav.home')
|
|
578
|
+
// with the namespace chosen elsewhere (useTranslation('common')), and
|
|
579
|
+
// an explicit t('common:nav.home') is reduced to its key below.
|
|
580
|
+
// Docusaurus keeps its historical lookup (no flat source file there).
|
|
531
581
|
let localeKeys = {};
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
582
|
+
let namespaces = new Set();
|
|
583
|
+
if (config.format !== 'docusaurus') {
|
|
584
|
+
const layout = discoverLocaleLayout(config, { cwd });
|
|
585
|
+
namespaces = new Set(layout.sourceFiles.map(f => f.ns).filter(Boolean));
|
|
586
|
+
for (const file of layout.sourceFiles) {
|
|
587
|
+
if (!fs.existsSync(file.path)) continue;
|
|
588
|
+
Object.assign(localeKeys, readLocaleFlat(file));
|
|
589
|
+
}
|
|
535
590
|
}
|
|
536
591
|
const localeEntries = Object.entries(localeKeys);
|
|
537
592
|
|
|
@@ -560,7 +615,14 @@ async function runLint(options = {}) {
|
|
|
560
615
|
}
|
|
561
616
|
}
|
|
562
617
|
|
|
563
|
-
// 5. Cross-reference
|
|
618
|
+
// 5. Cross-reference. i18next's "ns:key" form names a key inside one
|
|
619
|
+
// namespace file — compare its key part against the merged key set.
|
|
620
|
+
if (namespaces.size > 0) {
|
|
621
|
+
for (const ref of [...allI18nKeys]) {
|
|
622
|
+
const i = ref.indexOf(':');
|
|
623
|
+
if (i > 0 && namespaces.has(ref.slice(0, i))) allI18nKeys.add(ref.slice(i + 1));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
564
626
|
const deadKeys = localeEntries.length > 0
|
|
565
627
|
? findDeadKeys(allI18nKeys, localeKeys)
|
|
566
628
|
: [];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* local-only-marks.js — the files in a project a data steward has marked
|
|
3
|
+
* local-only: a `<file>.champollion.json` sidecar saying
|
|
4
|
+
* `"transmission": "local-only"` (what `champollion network register-corpus
|
|
5
|
+
* --data … --tier local-only` writes, and what the harness and nmt-forge
|
|
6
|
+
* honour: only a model on this machine may see that file).
|
|
7
|
+
*
|
|
8
|
+
* WHY init reads them: a project holding a local-only test set was set up
|
|
9
|
+
* with a hosted model by default — `init --yes` wrote OpenRouter beside a
|
|
10
|
+
* set its steward had said must never leave the machine (synthetic hospital
|
|
11
|
+
* persona, Rounds 10, 12 and 14). A mark in the project is a statement about
|
|
12
|
+
* where this project's text may go, so init's DEFAULT follows it: the local
|
|
13
|
+
* method. An explicit --method still wins; a hosted method is then the
|
|
14
|
+
* user's own choice, never a default.
|
|
15
|
+
*
|
|
16
|
+
* Read only: the walk opens sidecars (small JSON files) and nothing else;
|
|
17
|
+
* the data files they describe are never read.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
/** The sidecar suffix — mt_eval_harness.corpus_loader.SIDECAR_SUFFIX (keep in step). */
|
|
24
|
+
export const SIDECAR_SUFFIX = '.champollion.json';
|
|
25
|
+
|
|
26
|
+
/** The one transmission value a sidecar sets (corpus_loader.LOCAL_ONLY). */
|
|
27
|
+
export const LOCAL_ONLY = 'local-only';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Folders never searched: dependency trees, VCS metadata, virtualenvs,
|
|
31
|
+
* build caches and champollion's own cache. A steward's data sits in the
|
|
32
|
+
* project's own folders (data/, private/, test-sets/ …), not in these —
|
|
33
|
+
* and node_modules can be enormous (or, through symlinked packages, cyclic).
|
|
34
|
+
*/
|
|
35
|
+
const SKIP_DIRS = new Set([
|
|
36
|
+
'node_modules', '.git', '.hg', '.svn', '.champollion', '.venv', 'venv',
|
|
37
|
+
'__pycache__', '.next', '.nuxt', '.svelte-kit', '.turbo', '.cache',
|
|
38
|
+
'.pytest_cache', '.mypy_cache', '.tox',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/** How deep, and how many entries, the walk looks at — bounded, so init stays fast. */
|
|
42
|
+
const MAX_DEPTH = 8;
|
|
43
|
+
const MAX_ENTRIES = 50000;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Every local-only mark under `root`, nearest first (by depth, then path).
|
|
47
|
+
*
|
|
48
|
+
* A sidecar that cannot be read is reported as a mark (`unreadable: true`):
|
|
49
|
+
* the harness refuses to run a corpus whose sidecar it cannot read, and a
|
|
50
|
+
* steward wrote it to restrict the data — an unreadable restriction is never
|
|
51
|
+
* read as no restriction.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} root - The project folder (init's cwd)
|
|
54
|
+
* @returns {{ marks: Array<{ sidecar: string, dataFile: string, unreadable: boolean }>, truncated: boolean }}
|
|
55
|
+
* absolute paths; `truncated` when the walk hit its bound before finishing
|
|
56
|
+
*/
|
|
57
|
+
export function findLocalOnlyMarks(root) {
|
|
58
|
+
const marks = [];
|
|
59
|
+
let seen = 0;
|
|
60
|
+
let truncated = false;
|
|
61
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
62
|
+
while (queue.length > 0) {
|
|
63
|
+
const { dir, depth } = queue.shift();
|
|
64
|
+
let entries;
|
|
65
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
66
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
67
|
+
for (const e of entries) {
|
|
68
|
+
if (++seen > MAX_ENTRIES) { truncated = true; break; }
|
|
69
|
+
const full = path.join(dir, e.name);
|
|
70
|
+
// Directory symlinks are not followed (no cycles, no leaving the project).
|
|
71
|
+
if (e.isDirectory()) {
|
|
72
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
73
|
+
if (depth + 1 > MAX_DEPTH) { truncated = true; continue; }
|
|
74
|
+
queue.push({ dir: full, depth: depth + 1 });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!e.name.endsWith(SIDECAR_SUFFIX) || e.name === SIDECAR_SUFFIX) continue;
|
|
78
|
+
if (!(e.isFile() || e.isSymbolicLink())) continue;
|
|
79
|
+
const mark = readMark(full);
|
|
80
|
+
if (mark) marks.push({ ...mark, depth });
|
|
81
|
+
}
|
|
82
|
+
if (truncated && seen > MAX_ENTRIES) break;
|
|
83
|
+
}
|
|
84
|
+
marks.sort((a, b) => a.depth - b.depth || a.sidecar.localeCompare(b.sidecar));
|
|
85
|
+
return { marks: marks.map(({ depth, ...m }) => m), truncated };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* One sidecar's mark, or null when it marks nothing local-only.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} sidecar - Absolute path of a `<file>.champollion.json`
|
|
92
|
+
* @returns {{ sidecar: string, dataFile: string, unreadable: boolean }|null}
|
|
93
|
+
*/
|
|
94
|
+
export function readMark(sidecar) {
|
|
95
|
+
const dataFile = sidecar.slice(0, -SIDECAR_SUFFIX.length);
|
|
96
|
+
let data;
|
|
97
|
+
try {
|
|
98
|
+
data = JSON.parse(fs.readFileSync(sidecar, 'utf-8').replace(/^/, ''));
|
|
99
|
+
} catch {
|
|
100
|
+
return { sidecar, dataFile, unreadable: true };
|
|
101
|
+
}
|
|
102
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return { sidecar, dataFile, unreadable: true };
|
|
103
|
+
return String(data.transmission || '').trim().toLowerCase() === LOCAL_ONLY
|
|
104
|
+
? { sidecar, dataFile, unreadable: false }
|
|
105
|
+
: null;
|
|
106
|
+
}
|