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,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-scope.js — `sync --files <glob>` and `sync --retranslate <glob>`.
|
|
3
|
+
*
|
|
4
|
+
* Dogfood 2026-08-28, finding 3: the only way to translate a subset of
|
|
5
|
+
* files was hand-editing .champollion-content.lock — and deleting a lock
|
|
6
|
+
* entry did not even work, because the sync then ADOPTED the existing
|
|
7
|
+
* target as hand-translated instead of re-translating it.
|
|
8
|
+
*
|
|
9
|
+
* --files <glob> Only these content files are considered this run
|
|
10
|
+
* (the rest are untouched, not even scanned).
|
|
11
|
+
* --retranslate <glob> These content files are translated FRESH: the
|
|
12
|
+
* lock, the adoption rule and the Translation
|
|
13
|
+
* Memory are all bypassed for them. You named the
|
|
14
|
+
* file, so this is the one place a run overwrites a
|
|
15
|
+
* target it would otherwise keep — and pays for it.
|
|
16
|
+
*
|
|
17
|
+
* Patterns match the path the sync prints for a file: relative to the
|
|
18
|
+
* content directory for Hugo ("posts/hello.md"), and prefixed with the
|
|
19
|
+
* content folder for Docusaurus ("docs/intro.md", "blog/launch.md").
|
|
20
|
+
* A contentDir project ALSO matches the path from the project root
|
|
21
|
+
* ("newsletter/2026-10.md" for newsletter/2026-10.md): the repair hint a
|
|
22
|
+
* sync printed (`--redo files:2026-10.md --fresh`) failed with "No content
|
|
23
|
+
* file matches", and so did the root-relative path a person types next
|
|
24
|
+
* (Round 6, school persona).
|
|
25
|
+
* `*` stays within one folder, `**` crosses any number of folders. Both
|
|
26
|
+
* flags repeat. A pattern that matches no source file is an error, raised
|
|
27
|
+
* before anything is spent — a typo must not turn into "nothing happened".
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { compilePattern } from './no-translate.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} FileScope
|
|
34
|
+
* @property {(label: string) => boolean} includes - Is this file in the run?
|
|
35
|
+
* @property {(label: string) => boolean} retranslates - Translate it fresh?
|
|
36
|
+
* @property {() => void} assertAllMatched - Throw if a pattern matched nothing
|
|
37
|
+
* @property {boolean} limitsFiles - True when --files was given
|
|
38
|
+
* @property {string[]} retranslatePatterns
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
function toList(value) {
|
|
42
|
+
if (value == null) return [];
|
|
43
|
+
return (Array.isArray(value) ? value : [value])
|
|
44
|
+
.flatMap(v => String(v).split(','))
|
|
45
|
+
.map(v => v.trim().replace(/^\.\//, '').replace(/\\/g, '/'))
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the run's file scope from CLI args. Returns null when neither flag
|
|
51
|
+
* was given (the common case costs nothing).
|
|
52
|
+
*
|
|
53
|
+
* @param {object} cliArgs
|
|
54
|
+
* @param {{ rootPrefix?: string }} [options] - rootPrefix: the content
|
|
55
|
+
* directory relative to the project root ("newsletter"); a pattern then
|
|
56
|
+
* also matches `<rootPrefix>/<label>`
|
|
57
|
+
* @returns {FileScope|null}
|
|
58
|
+
*/
|
|
59
|
+
export function compileFileScope(cliArgs = {}, { rootPrefix = '' } = {}) {
|
|
60
|
+
const filesPatterns = toList(cliArgs.files);
|
|
61
|
+
const retranslatePatterns = toList(cliArgs.retranslate);
|
|
62
|
+
if (filesPatterns.length === 0 && retranslatePatterns.length === 0) return null;
|
|
63
|
+
|
|
64
|
+
const compile = (patterns) => patterns.map(p => ({ pattern: p, test: compilePattern(p, '/'), hits: 0 }));
|
|
65
|
+
const files = compile(filesPatterns);
|
|
66
|
+
const retranslate = compile(retranslatePatterns);
|
|
67
|
+
|
|
68
|
+
const rawPrefix = String(rootPrefix || '').replace(/\\/g, '/').replace(/^(\.\/)+/, '').replace(/\/+$/, '');
|
|
69
|
+
const prefix = rawPrefix === '.' ? '' : rawPrefix;
|
|
70
|
+
const matchAny = (list, label) => {
|
|
71
|
+
const rel = label.replace(/\\/g, '/');
|
|
72
|
+
const forms = [rel.split('/')];
|
|
73
|
+
if (prefix) forms.push(`${prefix}/${rel}`.split('/'));
|
|
74
|
+
let hit = false;
|
|
75
|
+
for (const m of list) {
|
|
76
|
+
if (forms.some(segments => m.test(segments))) { m.hits++; hit = true; }
|
|
77
|
+
}
|
|
78
|
+
return hit;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
limitsFiles: files.length > 0,
|
|
83
|
+
retranslatePatterns,
|
|
84
|
+
retranslates: (label) => matchAny(retranslate, label),
|
|
85
|
+
// A --retranslate file is always in the run, even outside --files. Both
|
|
86
|
+
// lists are consulted every time: a short-circuit here left every
|
|
87
|
+
// --retranslate pattern (= `--redo files:<glob> --fresh`) with zero hits
|
|
88
|
+
// when no --files was given, and the run failed "No content file
|
|
89
|
+
// matches" for a file that exists (Round 6, school persona).
|
|
90
|
+
includes: (label) => {
|
|
91
|
+
const inRetranslate = matchAny(retranslate, label);
|
|
92
|
+
const inFiles = files.length === 0 || matchAny(files, label);
|
|
93
|
+
return inFiles || inRetranslate;
|
|
94
|
+
},
|
|
95
|
+
assertAllMatched() {
|
|
96
|
+
const dead = [...files, ...retranslate].filter(m => m.hits === 0).map(m => m.pattern);
|
|
97
|
+
if (dead.length > 0) {
|
|
98
|
+
const e = new Error(
|
|
99
|
+
`No content file matches ${dead.map(p => JSON.stringify(p)).join(', ')}. ` +
|
|
100
|
+
`Patterns match the paths sync prints, e.g. "docs/intro.md" or "posts/**"${prefix ? `, or the path from the project root ("${prefix}/…")` : ''}. Nothing was translated.`);
|
|
101
|
+
e.code = 'CHAMPOLLION_NO_FILE_MATCH';
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
package/lib/flatten.js
CHANGED
|
@@ -32,9 +32,57 @@ function flattenKeys(obj, prefix = '') {
|
|
|
32
32
|
return keys;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** An i18next plural key: `<base>[_ordinal]_<category>` (lib/plurals.js). */
|
|
36
|
+
const PLURAL_KEY_RE = /^(.+?)(_ordinal)?_(zero|one|two|few|many|other)$/;
|
|
37
|
+
/** CLDR's order of the plural categories. */
|
|
38
|
+
const PLURAL_ORDER = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Put `key` into `obj`. An existing key keeps its place. A NEW plural form
|
|
42
|
+
* (`count_many`) goes beside its siblings (`count_one`, `count_other`, same
|
|
43
|
+
* base, cardinal or ordinal alike) in CLDR order — before the first sibling
|
|
44
|
+
* whose category comes after it, else after the last sibling — and nothing
|
|
45
|
+
* else in the object moves. Any other new key goes at the end, as before.
|
|
46
|
+
*
|
|
47
|
+
* WHY: `verify`'s repair for a missing `count_many` appended it after
|
|
48
|
+
* `count_other`, so French and Spanish files ordered their plural keys
|
|
49
|
+
* differently (Round 10, i18next persona). A fresh file is written in the
|
|
50
|
+
* expansion's CLDR order; a restored form now lands where it would have been.
|
|
51
|
+
*
|
|
52
|
+
* Works on a nested object's level (the leaf name) and on a flat map
|
|
53
|
+
* (`cart.count_many` — the base includes the dotted path).
|
|
54
|
+
*
|
|
55
|
+
* @param {object} obj
|
|
56
|
+
* @param {string} key
|
|
57
|
+
* @param {*} value
|
|
58
|
+
*/
|
|
59
|
+
function assignInOrder(obj, key, value) {
|
|
60
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) { obj[key] = value; return; }
|
|
61
|
+
const m = PLURAL_KEY_RE.exec(key);
|
|
62
|
+
if (!m) { obj[key] = value; return; }
|
|
63
|
+
const rank = PLURAL_ORDER.indexOf(m[3]);
|
|
64
|
+
const keys = Object.keys(obj);
|
|
65
|
+
let at = -1; // insert before keys[at]
|
|
66
|
+
let lastSibling = -1;
|
|
67
|
+
for (let i = 0; i < keys.length; i++) {
|
|
68
|
+
const s = PLURAL_KEY_RE.exec(keys[i]);
|
|
69
|
+
if (!s || s[1] !== m[1] || !!s[2] !== !!m[2]) continue;
|
|
70
|
+
if (PLURAL_ORDER.indexOf(s[3]) > rank) { at = i; break; }
|
|
71
|
+
lastSibling = i;
|
|
72
|
+
}
|
|
73
|
+
if (at < 0 && lastSibling < 0) { obj[key] = value; return; }
|
|
74
|
+
const insertAt = at >= 0 ? at : lastSibling + 1;
|
|
75
|
+
if (insertAt >= keys.length) { obj[key] = value; return; }
|
|
76
|
+
const entries = Object.entries(obj);
|
|
77
|
+
entries.splice(insertAt, 0, [key, value]);
|
|
78
|
+
for (const k of keys) delete obj[k];
|
|
79
|
+
for (const [k, v] of entries) obj[k] = v;
|
|
80
|
+
}
|
|
81
|
+
|
|
35
82
|
/**
|
|
36
83
|
* Set a value in a nested object using a dot-notation path.
|
|
37
|
-
* Creates intermediate objects as needed.
|
|
84
|
+
* Creates intermediate objects as needed. A new plural form is placed among
|
|
85
|
+
* its siblings in CLDR order (assignInOrder); other new keys go at the end.
|
|
38
86
|
*
|
|
39
87
|
* @param {object} obj - Target nested object
|
|
40
88
|
* @param {string} dotPath - Dot-notation path like "pages.about.title"
|
|
@@ -49,7 +97,36 @@ function setNestedValue(obj, dotPath, value) {
|
|
|
49
97
|
}
|
|
50
98
|
current = current[parts[i]];
|
|
51
99
|
}
|
|
52
|
-
current
|
|
100
|
+
assignInOrder(current, parts[parts.length - 1], value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Delete a dot-notation path from a nested object. Parents are left in
|
|
105
|
+
* place (they usually hold sibling keys); a parent left EMPTY by the delete
|
|
106
|
+
* is removed too, so no `{}` husk is written back to the locale file.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} obj - Target nested object
|
|
109
|
+
* @param {string} dotPath - Dot-notation path like "cart.item_one"
|
|
110
|
+
* @returns {boolean} True when a value was removed
|
|
111
|
+
*/
|
|
112
|
+
function deleteNestedValue(obj, dotPath) {
|
|
113
|
+
const parts = dotPath.split('.');
|
|
114
|
+
const chain = [obj];
|
|
115
|
+
let current = obj;
|
|
116
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
117
|
+
const next = current[parts[i]];
|
|
118
|
+
if (typeof next !== 'object' || next === null || Array.isArray(next)) return false;
|
|
119
|
+
chain.push(next);
|
|
120
|
+
current = next;
|
|
121
|
+
}
|
|
122
|
+
const leaf = parts[parts.length - 1];
|
|
123
|
+
if (!Object.prototype.hasOwnProperty.call(current, leaf)) return false;
|
|
124
|
+
delete current[leaf];
|
|
125
|
+
for (let i = chain.length - 1; i > 0; i--) {
|
|
126
|
+
if (Object.keys(chain[i]).length > 0) break;
|
|
127
|
+
delete chain[i - 1][parts[i - 1]];
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
53
130
|
}
|
|
54
131
|
|
|
55
|
-
export { flattenKeys, setNestedValue };
|
|
132
|
+
export { flattenKeys, setNestedValue, deleteNestedValue, assignInOrder };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flutter-locales.js — does Flutter's own UI text cover a target locale?
|
|
3
|
+
*
|
|
4
|
+
* `flutter gen-l10n` builds the app's messages from the ARB files sync
|
|
5
|
+
* writes, but Material and Cupertino widgets (date pickers, "Back",
|
|
6
|
+
* "Cancel", text direction) take their own text from flutter_localizations
|
|
7
|
+
* (GlobalMaterialLocalizations, GlobalCupertinoLocalizations,
|
|
8
|
+
* GlobalWidgetsLocalizations), which cover a fixed list of languages. An app
|
|
9
|
+
* whose supportedLocales include one outside that list — a private-use code
|
|
10
|
+
* such as `qaa`, most low-resource languages — fails at runtime ("No
|
|
11
|
+
* MaterialLocalizations found") unless it adds a fallback delegate (Round
|
|
12
|
+
* 11, hospital persona: neither init nor the Flutter docs said so).
|
|
13
|
+
*
|
|
14
|
+
* The list is Flutter's own, read from the Flutter SDK on this machine
|
|
15
|
+
* (FLUTTER_ROOT, or the `flutter` on PATH): one
|
|
16
|
+
* `packages/flutter_localizations/lib/src/l10n/material_<locale>.arb` file
|
|
17
|
+
* per covered locale. Nothing here guesses it: without an SDK, a private-use
|
|
18
|
+
* code is reported as uncovered (no list can hold one) and every other code
|
|
19
|
+
* is reported as unchecked, with where to read the rule.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { isPrivateUseCode } from './registers.js';
|
|
25
|
+
|
|
26
|
+
/** Where the CLI's Flutter section explains the fallback delegate. */
|
|
27
|
+
export const FLUTTER_LOCALES_DOC = 'https://champollion.dev/docs/integrations/frameworks#flutter-locales-outside-flutters-own-list';
|
|
28
|
+
|
|
29
|
+
/** The directory of Flutter's material_<locale>.arb files, from an SDK root. */
|
|
30
|
+
const L10N_DIR = path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The Flutter SDK root on this machine: FLUTTER_ROOT, else the directory
|
|
34
|
+
* above the `bin/` of the first `flutter` on PATH (symlinks resolved).
|
|
35
|
+
*
|
|
36
|
+
* @param {object} [env=process.env]
|
|
37
|
+
* @returns {string|null}
|
|
38
|
+
*/
|
|
39
|
+
export function findFlutterSdk(env = process.env) {
|
|
40
|
+
const isSdk = (root) => !!root && fs.existsSync(path.join(root, L10N_DIR));
|
|
41
|
+
if (env.FLUTTER_ROOT && isSdk(env.FLUTTER_ROOT)) return env.FLUTTER_ROOT;
|
|
42
|
+
for (const dir of String(env.PATH || '').split(path.delimiter)) {
|
|
43
|
+
if (!dir) continue;
|
|
44
|
+
for (const name of process.platform === 'win32' ? ['flutter.bat', 'flutter'] : ['flutter']) {
|
|
45
|
+
const exe = path.join(dir, name);
|
|
46
|
+
let real;
|
|
47
|
+
try { real = fs.realpathSync(exe); } catch { continue; }
|
|
48
|
+
const root = path.dirname(path.dirname(real));
|
|
49
|
+
if (isSdk(root)) return root;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The language codes Flutter's Material localizations cover, read from the
|
|
57
|
+
* SDK: `material_pt_BR.arb` → "pt". null when no SDK is found.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} [env=process.env]
|
|
60
|
+
* @returns {{ languages: Set<string>, from: string }|null}
|
|
61
|
+
*/
|
|
62
|
+
export function flutterMaterialLanguages(env = process.env) {
|
|
63
|
+
const root = findFlutterSdk(env);
|
|
64
|
+
if (!root) return null;
|
|
65
|
+
const dir = path.join(root, L10N_DIR);
|
|
66
|
+
let names;
|
|
67
|
+
try { names = fs.readdirSync(dir); } catch { return null; }
|
|
68
|
+
const languages = new Set();
|
|
69
|
+
for (const n of names) {
|
|
70
|
+
const m = /^material_([A-Za-z]{2,3})(?:_[A-Za-z0-9]+)*\.arb$/.exec(n);
|
|
71
|
+
if (m) languages.add(m[1].toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
return languages.size > 0 ? { languages, from: dir } : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** "pt_BR" / "pt-BR" → "pt". */
|
|
77
|
+
function languageOf(code) {
|
|
78
|
+
return String(code).split(/[-_]/)[0].toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Which ARB target locales Flutter's own widget text does not cover.
|
|
83
|
+
*
|
|
84
|
+
* @param {string[]} codes - Target locales as the ARB files name them
|
|
85
|
+
* @param {object} [env=process.env]
|
|
86
|
+
* @returns {{ uncovered: string[], unchecked: string[], from: string|null }}
|
|
87
|
+
* uncovered: outside Flutter's list (or private-use); unchecked: no SDK here to check against
|
|
88
|
+
*/
|
|
89
|
+
export function flutterUncoveredLocales(codes, env = process.env) {
|
|
90
|
+
const list = flutterMaterialLanguages(env);
|
|
91
|
+
const uncovered = [];
|
|
92
|
+
const unchecked = [];
|
|
93
|
+
for (const code of codes) {
|
|
94
|
+
// A private-use code (qaa–qtz) is in no published list.
|
|
95
|
+
if (isPrivateUseCode(code)) uncovered.push(code);
|
|
96
|
+
else if (!list) unchecked.push(code);
|
|
97
|
+
else if (!list.languages.has(languageOf(code))) uncovered.push(code);
|
|
98
|
+
}
|
|
99
|
+
return { uncovered, unchecked, from: list ? list.from : null };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The line(s) init and sync print for ARB targets: what to add for a locale
|
|
104
|
+
* Flutter does not cover, or — with no SDK here — that it was not checked.
|
|
105
|
+
* Empty when every target is covered.
|
|
106
|
+
*
|
|
107
|
+
* @param {string[]} codes
|
|
108
|
+
* @param {object} [env=process.env]
|
|
109
|
+
* @returns {Array<{ level: 'warn'|'info', text: string }>}
|
|
110
|
+
*/
|
|
111
|
+
export function flutterLocaleLines(codes, env = process.env) {
|
|
112
|
+
const { uncovered, unchecked, from } = flutterUncoveredLocales(codes, env);
|
|
113
|
+
const lines = [];
|
|
114
|
+
if (uncovered.length > 0) {
|
|
115
|
+
const basis = from ? `checked against ${from}` : 'a private-use code is in no list';
|
|
116
|
+
lines.push({ level: 'warn', text: `Flutter: its own Material/Cupertino widget text (GlobalMaterialLocalizations) does not cover ${uncovered.join(', ')} (${basis}). `
|
|
117
|
+
+ `An app with ${uncovered.length > 1 ? 'these locales' : 'this locale'} in supportedLocales fails at runtime without a fallback delegate for it — add one: ${FLUTTER_LOCALES_DOC}` });
|
|
118
|
+
}
|
|
119
|
+
if (unchecked.length > 0) {
|
|
120
|
+
lines.push({ level: 'info', text: `Flutter: no Flutter SDK found here (FLUTTER_ROOT, or flutter on PATH), so ${unchecked.join(', ')} ${unchecked.length > 1 ? 'were' : 'was'} not checked against `
|
|
121
|
+
+ `the languages Flutter's own widget text covers (GlobalMaterialLocalizations). A language outside that list needs a fallback delegate: ${FLUTTER_LOCALES_DOC}` });
|
|
122
|
+
}
|
|
123
|
+
return lines;
|
|
124
|
+
}
|