@x18n/core 0.0.13 → 0.0.15
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/dist/index.d.mts +284 -9
- package/dist/index.mjs +664 -67
- package/package.json +1 -1
- package/schema/config.json +58 -10
- package/schema/plugin.json +32 -10
- package/schema/reviews.json +98 -0
package/dist/index.mjs
CHANGED
|
@@ -138,7 +138,9 @@ function defineConfig(config) {
|
|
|
138
138
|
}
|
|
139
139
|
//#endregion
|
|
140
140
|
//#region src/errors.ts
|
|
141
|
-
|
|
141
|
+
function at(p) {
|
|
142
|
+
return p.line === void 0 ? p.path : `${p.path}:${p.line}`;
|
|
143
|
+
}
|
|
142
144
|
/**
|
|
143
145
|
* Every coded error. Groups: 0xxx config, 1xxx plugin, 2xxx discovery and parse,
|
|
144
146
|
* 3xxx scanning, 4xxx edits, 5xxx reviews. `fix` stays static so the docs can show it.
|
|
@@ -223,6 +225,22 @@ const errorDefinitions = {
|
|
|
223
225
|
X18N4008: {
|
|
224
226
|
why: (p) => `Invalid edit input: ${p.reason}`,
|
|
225
227
|
fix: "Check the input shape against the docs."
|
|
228
|
+
},
|
|
229
|
+
X18N5001: {
|
|
230
|
+
why: (p) => `Review file ${p.path} could not be read: ${p.reason}`,
|
|
231
|
+
fix: "Fix the file against `schema/reviews.json`, or restore it from git."
|
|
232
|
+
},
|
|
233
|
+
X18N5003: {
|
|
234
|
+
why: (p) => `Reviews are not available: ${p.reason}`,
|
|
235
|
+
fix: "Set `review.enabled` to true, and fix the review file if x18n reports it."
|
|
236
|
+
},
|
|
237
|
+
X18N5004: {
|
|
238
|
+
why: (p) => `Review not found: ${p.id}`,
|
|
239
|
+
fix: "List review ids with `x18n review list` (agents: the `x18n_reviews` tool)."
|
|
240
|
+
},
|
|
241
|
+
X18N5005: {
|
|
242
|
+
why: (p) => `Review ${p.id} is not an open suggestion`,
|
|
243
|
+
fix: "Apply only open reviews of type `suggestion`."
|
|
226
244
|
}
|
|
227
245
|
};
|
|
228
246
|
/** Raise with `throw errors.X18N2002({ path, reason })`. Core has no reporters: surfaces print what they catch. */
|
|
@@ -249,6 +267,24 @@ function toErrorInfo(error) {
|
|
|
249
267
|
};
|
|
250
268
|
}
|
|
251
269
|
//#endregion
|
|
270
|
+
//#region src/fuzzy.ts
|
|
271
|
+
/** Subsequence match score of `query` in `text`, case-insensitive; higher is better, -1 when it does not match. Substring hits rank above every subsequence. */
|
|
272
|
+
function fuzzyScore(query, text) {
|
|
273
|
+
const q = query.toLowerCase();
|
|
274
|
+
const s = text.toLowerCase();
|
|
275
|
+
const exact = s.indexOf(q);
|
|
276
|
+
if (exact !== -1) return 2e3 - Math.min(exact, 1e3);
|
|
277
|
+
let score = 0;
|
|
278
|
+
let at = -1;
|
|
279
|
+
for (const char of q) {
|
|
280
|
+
const next = s.indexOf(char, at + 1);
|
|
281
|
+
if (next === -1) return -1;
|
|
282
|
+
score += next === at + 1 ? 2 : 1;
|
|
283
|
+
at = next;
|
|
284
|
+
}
|
|
285
|
+
return score;
|
|
286
|
+
}
|
|
287
|
+
//#endregion
|
|
252
288
|
//#region src/keys.ts
|
|
253
289
|
/** Canonical string form of a key: `ns:a.b.c`, or `a.b.c` without namespace. */
|
|
254
290
|
function joinKey(namespace, path) {
|
|
@@ -331,6 +367,40 @@ function pathMatcher(globs) {
|
|
|
331
367
|
return (path) => regexes.some((re) => re.test(path));
|
|
332
368
|
}
|
|
333
369
|
//#endregion
|
|
370
|
+
//#region src/library.ts
|
|
371
|
+
/** One plugin holding a library's call specs, regexes, namespace rules, defaults and extraction templates. */
|
|
372
|
+
function createLibraryPlugin(options) {
|
|
373
|
+
return {
|
|
374
|
+
name: options.name,
|
|
375
|
+
async config(draft, ctx) {
|
|
376
|
+
if (options.namespace) draft.namespace = {
|
|
377
|
+
...draft.namespace,
|
|
378
|
+
separator: options.namespace.separator ?? draft.namespace.separator,
|
|
379
|
+
default: draft.namespace.default ?? options.namespace.default,
|
|
380
|
+
fromPath: options.namespace.fromPath ?? draft.namespace.fromPath
|
|
381
|
+
};
|
|
382
|
+
if (options.resources) draft.resources = [...draft.resources, ...options.resources.filter((spec) => !draft.resources.some((r) => r.pattern === spec.pattern))];
|
|
383
|
+
if (options.keyStyle && draft.keyStyle === "auto" && !ctx.user.keyStyle) draft.keyStyle = options.keyStyle;
|
|
384
|
+
draft.usage = {
|
|
385
|
+
...draft.usage,
|
|
386
|
+
calls: [...draft.usage.calls, ...options.calls ?? []],
|
|
387
|
+
regex: [...draft.usage.regex, ...options.regex ?? []],
|
|
388
|
+
derivedKeys: [...draft.usage.derivedKeys, ...options.derivedKeys ?? []]
|
|
389
|
+
};
|
|
390
|
+
await options.extra?.(draft, ctx);
|
|
391
|
+
},
|
|
392
|
+
placeholders: options.placeholders,
|
|
393
|
+
localeFinders: options.finders,
|
|
394
|
+
normalizeKey: options.normalizeKey,
|
|
395
|
+
transformResource: options.transformResource,
|
|
396
|
+
translatePrompt: options.translatePrompt,
|
|
397
|
+
extractors: options.templates ? [{
|
|
398
|
+
id: options.name,
|
|
399
|
+
templates: options.templates
|
|
400
|
+
}] : void 0
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
//#endregion
|
|
334
404
|
//#region src/locale.ts
|
|
335
405
|
const defaultRegions = {
|
|
336
406
|
ar: "SA",
|
|
@@ -585,8 +655,8 @@ function uniqueById(items, kind) {
|
|
|
585
655
|
}
|
|
586
656
|
function createRegistry(input, config) {
|
|
587
657
|
const plugins = sortByEnforce(input);
|
|
588
|
-
const formats = plugins.flatMap((plugin) => plugin.formats ?? []);
|
|
589
|
-
const scanners = plugins.flatMap((plugin) => plugin.scanners ?? []);
|
|
658
|
+
const formats = plugins.flatMap((plugin) => [...plugin.formats ?? [], ...plugin.lazyFormats ?? []]);
|
|
659
|
+
const scanners = plugins.flatMap((plugin) => [...plugin.scanners ?? [], ...plugin.lazyScanners ?? []]);
|
|
590
660
|
const finders = [...uniqueById(plugins.flatMap((plugin) => plugin.localeFinders ?? []), "Locale finder").values()];
|
|
591
661
|
const checks = uniqueById(plugins.flatMap((plugin) => plugin.checks ?? []), "Check");
|
|
592
662
|
const reporters = uniqueById(plugins.flatMap((plugin) => plugin.reporters ?? []), "Reporter");
|
|
@@ -602,8 +672,58 @@ function createRegistry(input, config) {
|
|
|
602
672
|
});
|
|
603
673
|
else byExtension.set(extension, format);
|
|
604
674
|
}
|
|
605
|
-
const
|
|
675
|
+
const isLazy = (format) => "load" in format;
|
|
676
|
+
const loaded = new Map(formats.filter((format) => !isLazy(format)).map((format) => [format.id, format]));
|
|
677
|
+
const loading = /* @__PURE__ */ new Map();
|
|
678
|
+
const formatById = (id) => loaded.get(id);
|
|
679
|
+
const formatIdFor = (path, hint) => {
|
|
680
|
+
if (hint) return hint;
|
|
681
|
+
const extension = extensionOf(path);
|
|
682
|
+
return config.formats[extension] ?? byExtension.get(extension)?.id;
|
|
683
|
+
};
|
|
684
|
+
const loadFormat = (id) => {
|
|
685
|
+
const ready = loaded.get(id);
|
|
686
|
+
if (ready) return Promise.resolve(ready);
|
|
687
|
+
const lazy = formats.find((format) => format.id === id);
|
|
688
|
+
if (!lazy || !isLazy(lazy)) return Promise.resolve(void 0);
|
|
689
|
+
let pending = loading.get(id);
|
|
690
|
+
if (!pending) {
|
|
691
|
+
pending = lazy.load().then((mod) => {
|
|
692
|
+
const format = mod.default().formats?.find((item) => item.id === id && !isLazy(item));
|
|
693
|
+
if (format) loaded.set(id, format);
|
|
694
|
+
return format;
|
|
695
|
+
});
|
|
696
|
+
loading.set(id, pending);
|
|
697
|
+
}
|
|
698
|
+
return pending;
|
|
699
|
+
};
|
|
606
700
|
const scannerCache = /* @__PURE__ */ new Map();
|
|
701
|
+
const loadedScanners = new Map(scanners.filter((scanner) => !("load" in scanner)).map((scanner) => [scanner.id, scanner]));
|
|
702
|
+
const loadingScanners = /* @__PURE__ */ new Map();
|
|
703
|
+
const loadScanner = (entry) => {
|
|
704
|
+
if (!("load" in entry)) return Promise.resolve(entry);
|
|
705
|
+
const ready = loadedScanners.get(entry.id);
|
|
706
|
+
if (ready) return Promise.resolve(ready);
|
|
707
|
+
let pending = loadingScanners.get(entry.id);
|
|
708
|
+
if (!pending) {
|
|
709
|
+
pending = entry.load().then((mod) => {
|
|
710
|
+
const scanner = mod.default().scanners?.find((item) => item.id === entry.id);
|
|
711
|
+
if (scanner) loadedScanners.set(entry.id, scanner);
|
|
712
|
+
return scanner;
|
|
713
|
+
});
|
|
714
|
+
loadingScanners.set(entry.id, pending);
|
|
715
|
+
}
|
|
716
|
+
return pending;
|
|
717
|
+
};
|
|
718
|
+
const scannersFor = (path) => {
|
|
719
|
+
const extension = extensionOf(path);
|
|
720
|
+
let list = scannerCache.get(extension);
|
|
721
|
+
if (!list) {
|
|
722
|
+
list = extension ? scanners.filter((scanner) => scanner.extensions.includes(extension) || scanner.extensions.includes("*")) : [];
|
|
723
|
+
scannerCache.set(extension, list);
|
|
724
|
+
}
|
|
725
|
+
return list;
|
|
726
|
+
};
|
|
607
727
|
return {
|
|
608
728
|
plugins,
|
|
609
729
|
formats,
|
|
@@ -615,22 +735,21 @@ function createRegistry(input, config) {
|
|
|
615
735
|
manifests: plugins.flatMap((plugin) => plugin.manifests ?? []),
|
|
616
736
|
extractors: plugins.flatMap((plugin) => plugin.extractors ?? []),
|
|
617
737
|
histories: plugins.flatMap((plugin) => plugin.histories ?? []),
|
|
738
|
+
reviewStores: plugins.flatMap((plugin) => plugin.reviewStores ?? []),
|
|
618
739
|
conflicts,
|
|
619
740
|
formatById,
|
|
741
|
+
formatIdFor,
|
|
620
742
|
formatFor(path, hint) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const mapped = config.formats[extension];
|
|
624
|
-
return mapped ? formatById(mapped) : byExtension.get(extension);
|
|
743
|
+
const id = formatIdFor(path, hint);
|
|
744
|
+
return id === void 0 ? void 0 : formatById(id);
|
|
625
745
|
},
|
|
626
|
-
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
return list;
|
|
746
|
+
async loadFormatFor(path, hint) {
|
|
747
|
+
const id = formatIdFor(path, hint);
|
|
748
|
+
return id === void 0 ? void 0 : loadFormat(id);
|
|
749
|
+
},
|
|
750
|
+
scannersFor,
|
|
751
|
+
async loadScannersFor(path) {
|
|
752
|
+
return (await Promise.all(scannersFor(path).map(loadScanner))).filter((scanner) => scanner !== void 0);
|
|
634
753
|
}
|
|
635
754
|
};
|
|
636
755
|
}
|
|
@@ -665,7 +784,7 @@ const callSpec = v.strictObject({
|
|
|
665
784
|
"accessor"
|
|
666
785
|
])),
|
|
667
786
|
key: v.optional(v.union([v.number(), argSource])),
|
|
668
|
-
namespace: v.optional(argSource),
|
|
787
|
+
namespace: v.optional(v.union([argSource, v.string()])),
|
|
669
788
|
keyPrefix: v.optional(argSource),
|
|
670
789
|
factory: v.optional(v.boolean()),
|
|
671
790
|
returns: v.optional(strings),
|
|
@@ -724,12 +843,18 @@ const checkSetting = v.union([level, v.tuple([level, jsonObject])]);
|
|
|
724
843
|
const namespaceOptions = v.strictObject({
|
|
725
844
|
separator: v.optional(v.string()),
|
|
726
845
|
default: v.optional(v.string()),
|
|
846
|
+
fallback: v.optional(strings),
|
|
847
|
+
byFile: v.optional(v.record(v.string(), v.string())),
|
|
727
848
|
fromPath: v.optional(v.picklist([
|
|
728
849
|
"dot",
|
|
729
850
|
"slash",
|
|
730
851
|
"camel"
|
|
731
852
|
]))
|
|
732
853
|
});
|
|
854
|
+
const reviewAuthor = v.strictObject({
|
|
855
|
+
name: v.optional(v.string()),
|
|
856
|
+
email: v.optional(v.string())
|
|
857
|
+
});
|
|
733
858
|
/** Validates YAML and JSON configs, and the serializable part of TS configs. */
|
|
734
859
|
const ConfigSchema = v.strictObject({
|
|
735
860
|
$schema: v.optional(v.string()),
|
|
@@ -769,6 +894,11 @@ const ConfigSchema = v.strictObject({
|
|
|
769
894
|
ignoreKeys: v.optional(strings),
|
|
770
895
|
glossary: v.optional(v.array(glossaryEntry)),
|
|
771
896
|
history: v.optional(v.union([v.boolean(), v.record(v.string(), v.union([v.literal(false), v.looseObject({})]))])),
|
|
897
|
+
review: v.optional(v.strictObject({
|
|
898
|
+
enabled: v.optional(v.boolean()),
|
|
899
|
+
file: v.optional(v.string()),
|
|
900
|
+
author: v.optional(reviewAuthor)
|
|
901
|
+
})),
|
|
772
902
|
projects: v.optional(strings)
|
|
773
903
|
});
|
|
774
904
|
const PluginFileSchema = v.strictObject({
|
|
@@ -810,6 +940,27 @@ const PluginFileSchema = v.strictObject({
|
|
|
810
940
|
}))),
|
|
811
941
|
extract: v.optional(v.strictObject({ templates: v.optional(extractTemplates) }))
|
|
812
942
|
});
|
|
943
|
+
const reviewComment = v.strictObject({
|
|
944
|
+
id: v.string(),
|
|
945
|
+
type: v.picklist([
|
|
946
|
+
"comment",
|
|
947
|
+
"approve",
|
|
948
|
+
"request-change",
|
|
949
|
+
"suggestion"
|
|
950
|
+
]),
|
|
951
|
+
author: v.optional(reviewAuthor),
|
|
952
|
+
time: v.string(),
|
|
953
|
+
text: v.optional(v.string()),
|
|
954
|
+
suggestion: v.optional(v.string()),
|
|
955
|
+
value: v.optional(v.string()),
|
|
956
|
+
resolved: v.optional(v.boolean())
|
|
957
|
+
});
|
|
958
|
+
const ReviewFileSchema = v.strictObject({
|
|
959
|
+
$schema: v.optional(v.string()),
|
|
960
|
+
x18n: v.literal(1),
|
|
961
|
+
threads: v.optional(v.record(v.string(), v.record(v.string(), v.array(reviewComment)))),
|
|
962
|
+
sources: v.optional(v.record(v.string(), v.record(v.string(), v.string())))
|
|
963
|
+
});
|
|
813
964
|
/** Validates data against a schema and returns readable issues instead of throwing. */
|
|
814
965
|
function validate(schema, data) {
|
|
815
966
|
const result = v.safeParse(schema, data);
|
|
@@ -889,6 +1040,28 @@ function placeholderProblems(project, ops) {
|
|
|
889
1040
|
});
|
|
890
1041
|
}
|
|
891
1042
|
//#endregion
|
|
1043
|
+
//#region src/project/explain.ts
|
|
1044
|
+
/** `project.explain()` as the text `x18n doctor` prints. `root` and `configFile` are shown as given. */
|
|
1045
|
+
function renderExplanation(e, configFile) {
|
|
1046
|
+
const lines = [
|
|
1047
|
+
`# ${e.name} (${e.root})`,
|
|
1048
|
+
`config: ${configFile ?? "none (detection only)"}`,
|
|
1049
|
+
"presets:"
|
|
1050
|
+
];
|
|
1051
|
+
const applied = e.presets.filter((p) => p.reason === "detected" || p.reason === "listed");
|
|
1052
|
+
lines.push(...applied.map((p) => ` ✓ ${p.name} (${p.reason})`));
|
|
1053
|
+
if (applied.length === 0) lines.push(" none");
|
|
1054
|
+
if (e.presets.length > applied.length) lines.push(` ${e.presets.length - applied.length} others not applied`);
|
|
1055
|
+
lines.push("finders:");
|
|
1056
|
+
for (const f of e.finders) lines.push(` ${f.id}: ${f.resources.length} resources`, ...f.resources.slice(0, 20).map((path) => ` ${path}`));
|
|
1057
|
+
lines.push("formats:", ...e.formats.filter((f) => f.resources > 0).map((f) => ` ${f.id} (${f.extensions.join(", ")}): ${f.resources} resources`));
|
|
1058
|
+
lines.push("scanners:", ...e.scanners.map((s) => ` ${s.id} (${s.extensions.join(", ") || "none"}): ${s.files} files`));
|
|
1059
|
+
if (e.conflicts.length) lines.push("conflicts:", ...e.conflicts.map((c) => ` .${c.extension}: ${c.winner} wins over ${c.loser}`));
|
|
1060
|
+
if (e.errors.length) lines.push("errors:", ...e.errors.map((err) => ` ${err.name} ${err.why}`));
|
|
1061
|
+
lines.push("config origins:", ...Object.entries(e.config.origins).map(([key, origin]) => ` ${key}: ${origin}`));
|
|
1062
|
+
return lines.join("\n");
|
|
1063
|
+
}
|
|
1064
|
+
//#endregion
|
|
892
1065
|
//#region src/project/history.ts
|
|
893
1066
|
const localChange = { updatedAt: Number.POSITIVE_INFINITY };
|
|
894
1067
|
/** A target message is stale when it last changed before its source message. */
|
|
@@ -949,8 +1122,12 @@ function innermost(scopes, range) {
|
|
|
949
1122
|
return best;
|
|
950
1123
|
}
|
|
951
1124
|
function createUsageResolver(config, namespaced, normalizers) {
|
|
952
|
-
const separator = config.namespace
|
|
953
|
-
|
|
1125
|
+
const { separator, fallback } = config.namespace;
|
|
1126
|
+
const byFile = Object.entries(config.namespace.byFile).map(([glob, namespace]) => ({
|
|
1127
|
+
matches: pathMatcher([glob]),
|
|
1128
|
+
namespace
|
|
1129
|
+
}));
|
|
1130
|
+
return { resolve(candidate, scopes, path, scanner, has) {
|
|
954
1131
|
const scope = innermost(scopes, candidate.range);
|
|
955
1132
|
const ctx = { path };
|
|
956
1133
|
let text = candidate.raw;
|
|
@@ -961,8 +1138,8 @@ function createUsageResolver(config, namespaced, normalizers) {
|
|
|
961
1138
|
break;
|
|
962
1139
|
}
|
|
963
1140
|
}
|
|
964
|
-
const scopeNamespace = candidate.namespace ?? scope?.namespace;
|
|
965
|
-
|
|
1141
|
+
const scopeNamespace = candidate.namespace ?? scope?.namespace ?? byFile.find((rule) => rule.matches(path))?.namespace;
|
|
1142
|
+
let scopePrefix = candidate.keyPrefix ?? scope?.keyPrefix;
|
|
966
1143
|
let namespace;
|
|
967
1144
|
let explicit = false;
|
|
968
1145
|
if (namespaced) {
|
|
@@ -973,10 +1150,17 @@ function createUsageResolver(config, namespaced, normalizers) {
|
|
|
973
1150
|
text = text.slice(index + separator.length);
|
|
974
1151
|
} else namespace = scopeNamespace ?? config.namespace.default;
|
|
975
1152
|
}
|
|
976
|
-
if (scopePrefix && !explicit)
|
|
1153
|
+
if (scopePrefix && !explicit) {
|
|
1154
|
+
const prefixed = text ? `${scopePrefix}.${text}` : scopePrefix;
|
|
1155
|
+
const lookup = candidate.dynamic === "prefix" ? prefixed.replace(/\.$/, "") : prefixed;
|
|
1156
|
+
if (candidate.keyPrefix || !scope?.optionalPrefix || has(joinKey(namespace, lookup.split(".")))) text = prefixed;
|
|
1157
|
+
else scopePrefix = void 0;
|
|
1158
|
+
}
|
|
1159
|
+
const keyPath = text === "" ? [] : text.split(".");
|
|
977
1160
|
let key;
|
|
978
1161
|
if (candidate.dynamic === "unknown") key = void 0;
|
|
979
|
-
else key = joinKey(namespace,
|
|
1162
|
+
else key = joinKey(namespace, keyPath);
|
|
1163
|
+
if (key !== void 0 && namespaced && !explicit && candidate.dynamic === false && !has(key)) key = fallback.map((ns) => joinKey(ns, keyPath)).find(has) ?? key;
|
|
980
1164
|
return {
|
|
981
1165
|
key,
|
|
982
1166
|
raw: candidate.raw,
|
|
@@ -1127,7 +1311,7 @@ function computeCoverage(state, options, sourceKeys) {
|
|
|
1127
1311
|
if (value === void 0 || value === "") {
|
|
1128
1312
|
if (inherited(key)) continue;
|
|
1129
1313
|
(value === void 0 ? missing : empty).push(key);
|
|
1130
|
-
} else if (locale !== sourceLocale && isOutdated(state.history(key, sourceLocale), state.history(key, locale))) outdated.push(key);
|
|
1314
|
+
} else if (locale !== sourceLocale && isOutdated(state.history(key, sourceLocale), state.history(key, locale)) && !state.fresh(key, locale)) outdated.push(key);
|
|
1131
1315
|
}
|
|
1132
1316
|
return {
|
|
1133
1317
|
locale,
|
|
@@ -1278,6 +1462,7 @@ function createAnalyzer(options) {
|
|
|
1278
1462
|
localeInfo: options.localeInfo,
|
|
1279
1463
|
text: options.text,
|
|
1280
1464
|
history: state.history,
|
|
1465
|
+
reviews: state.reviews,
|
|
1281
1466
|
*hardStrings() {
|
|
1282
1467
|
for (const [path, file] of state.files) for (const hard of file.result.hardStrings ?? []) yield {
|
|
1283
1468
|
...hard,
|
|
@@ -1305,6 +1490,161 @@ function createAnalyzer(options) {
|
|
|
1305
1490
|
return { analyze };
|
|
1306
1491
|
}
|
|
1307
1492
|
//#endregion
|
|
1493
|
+
//#region src/project/reviews.ts
|
|
1494
|
+
const noReviews = {
|
|
1495
|
+
threads: {},
|
|
1496
|
+
sources: {}
|
|
1497
|
+
};
|
|
1498
|
+
/** What a review compares: message values as text. */
|
|
1499
|
+
function valueText(value) {
|
|
1500
|
+
return value === null ? "" : String(value);
|
|
1501
|
+
}
|
|
1502
|
+
/** The hash recorded for a source value in `sources`. */
|
|
1503
|
+
function sourceHash(value) {
|
|
1504
|
+
return hashText(JSON.stringify(value));
|
|
1505
|
+
}
|
|
1506
|
+
function reviewState(comments) {
|
|
1507
|
+
const open = comments.filter((comment) => !comment.resolved);
|
|
1508
|
+
const decisive = open.findLast((comment) => comment.type === "approve" || comment.type === "request-change");
|
|
1509
|
+
if (decisive) return decisive.type === "request-change" ? "changes-requested" : decisive.stale ? "stale-approval" : "approved";
|
|
1510
|
+
if (open.some((comment) => comment.type === "suggestion")) return "suggested";
|
|
1511
|
+
return open.length > 0 ? "commented" : void 0;
|
|
1512
|
+
}
|
|
1513
|
+
/** A short random id that `taken` does not know. */
|
|
1514
|
+
function reviewId(taken) {
|
|
1515
|
+
let id;
|
|
1516
|
+
do
|
|
1517
|
+
id = Math.random().toString(36).slice(2, 8);
|
|
1518
|
+
while (id.length < 6 || taken(id));
|
|
1519
|
+
return id;
|
|
1520
|
+
}
|
|
1521
|
+
function byCode(a, b) {
|
|
1522
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
1523
|
+
}
|
|
1524
|
+
/** Threads in key order, then in the order of `locales`, then any other locale. */
|
|
1525
|
+
function listThreads(data, filter, locales, current) {
|
|
1526
|
+
const rank = (locale) => {
|
|
1527
|
+
const i = locales.indexOf(locale);
|
|
1528
|
+
return i < 0 ? locales.length : i;
|
|
1529
|
+
};
|
|
1530
|
+
const out = [];
|
|
1531
|
+
const keys = filter.key === void 0 ? Object.keys(data.threads).sort(byCode) : [filter.key];
|
|
1532
|
+
for (const key of keys) {
|
|
1533
|
+
const byLocale = data.threads[key] ?? {};
|
|
1534
|
+
const codes = Object.keys(byLocale).filter((locale) => filter.locale === void 0 || locale === filter.locale).sort((a, b) => rank(a) - rank(b) || byCode(a, b));
|
|
1535
|
+
for (const locale of codes) {
|
|
1536
|
+
const value = current(key, locale);
|
|
1537
|
+
const comments = byLocale[locale].filter((comment) => !filter.open || !comment.resolved).map((comment) => ({
|
|
1538
|
+
...comment,
|
|
1539
|
+
stale: comment.type === "approve" && comment.value !== void 0 && value !== void 0 && valueText(value) !== comment.value
|
|
1540
|
+
}));
|
|
1541
|
+
if (comments.length > 0) out.push({
|
|
1542
|
+
key,
|
|
1543
|
+
locale,
|
|
1544
|
+
comments,
|
|
1545
|
+
state: reviewState(comments)
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return out;
|
|
1550
|
+
}
|
|
1551
|
+
function findReview(data, id) {
|
|
1552
|
+
for (const [key, byLocale] of Object.entries(data.threads)) for (const [locale, comments] of Object.entries(byLocale)) {
|
|
1553
|
+
const comment = comments.find((c) => c.id === id);
|
|
1554
|
+
if (comment) return {
|
|
1555
|
+
key,
|
|
1556
|
+
locale,
|
|
1557
|
+
comment
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
function sortSection(section) {
|
|
1562
|
+
return Object.fromEntries(Object.entries(section).filter(([, byLocale]) => Object.keys(byLocale).length > 0).sort(([a], [b]) => byCode(a, b)).map(([key, byLocale]) => [key, Object.fromEntries(Object.entries(byLocale).sort(([a], [b]) => byCode(a, b)))]));
|
|
1563
|
+
}
|
|
1564
|
+
/** Sorted keys and locales, so edits from two branches merge well. */
|
|
1565
|
+
function sortReviews(data) {
|
|
1566
|
+
return {
|
|
1567
|
+
threads: sortSection(data.threads),
|
|
1568
|
+
sources: sortSection(data.sources)
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
function rekey(section, from, to) {
|
|
1572
|
+
const { [from]: entry, ...rest } = section;
|
|
1573
|
+
return entry ? {
|
|
1574
|
+
...rest,
|
|
1575
|
+
[to]: entry
|
|
1576
|
+
} : section;
|
|
1577
|
+
}
|
|
1578
|
+
function without(section, key, locales) {
|
|
1579
|
+
const { [key]: byLocale, ...rest } = section;
|
|
1580
|
+
if (!byLocale || locales === "all") return rest;
|
|
1581
|
+
return {
|
|
1582
|
+
...rest,
|
|
1583
|
+
[key]: Object.fromEntries(Object.entries(byLocale).filter(([locale]) => !locales.includes(locale)))
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
/** Review data being changed by an edit plan. */
|
|
1587
|
+
var ReviewDraft = class {
|
|
1588
|
+
data;
|
|
1589
|
+
changed = false;
|
|
1590
|
+
constructor(data) {
|
|
1591
|
+
this.data = data;
|
|
1592
|
+
}
|
|
1593
|
+
update(next) {
|
|
1594
|
+
this.changed = true;
|
|
1595
|
+
this.data = next;
|
|
1596
|
+
}
|
|
1597
|
+
thread(key, locale, comments) {
|
|
1598
|
+
const { threads } = this.data;
|
|
1599
|
+
this.update({
|
|
1600
|
+
...this.data,
|
|
1601
|
+
threads: {
|
|
1602
|
+
...threads,
|
|
1603
|
+
[key]: {
|
|
1604
|
+
...threads[key],
|
|
1605
|
+
[locale]: comments
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
add(key, locale, comment) {
|
|
1611
|
+
this.thread(key, locale, [...this.data.threads[key]?.[locale] ?? [], comment]);
|
|
1612
|
+
}
|
|
1613
|
+
resolve(id) {
|
|
1614
|
+
const found = findReview(this.data, id);
|
|
1615
|
+
if (found) this.thread(found.key, found.locale, this.data.threads[found.key][found.locale].map((c) => c.id === id ? {
|
|
1616
|
+
...c,
|
|
1617
|
+
resolved: true
|
|
1618
|
+
} : c));
|
|
1619
|
+
}
|
|
1620
|
+
record(key, locale, hash) {
|
|
1621
|
+
const { sources } = this.data;
|
|
1622
|
+
if (sources[key]?.[locale] !== hash) this.update({
|
|
1623
|
+
...this.data,
|
|
1624
|
+
sources: {
|
|
1625
|
+
...sources,
|
|
1626
|
+
[key]: {
|
|
1627
|
+
...sources[key],
|
|
1628
|
+
[locale]: hash
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
/** Threads and records follow a renamed key. */
|
|
1634
|
+
move(from, to) {
|
|
1635
|
+
if (this.data.threads[from] || this.data.sources[from]) this.update({
|
|
1636
|
+
threads: rekey(this.data.threads, from, to),
|
|
1637
|
+
sources: rekey(this.data.sources, from, to)
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
remove(key, locales) {
|
|
1641
|
+
if (this.data.threads[key] || this.data.sources[key]) this.update({
|
|
1642
|
+
threads: without(this.data.threads, key, locales),
|
|
1643
|
+
sources: without(this.data.sources, key, locales)
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
//#endregion
|
|
1308
1648
|
//#region src/project/planner.ts
|
|
1309
1649
|
const safeSegment = /^[\w-]+$/;
|
|
1310
1650
|
const safeFlat = /^[\w.-]+$/;
|
|
@@ -1345,7 +1685,8 @@ function mirrorPath(path, from, to) {
|
|
|
1345
1685
|
function importEdit(text, lines) {
|
|
1346
1686
|
const missing = lines.filter((line) => !text.includes(line.trim()));
|
|
1347
1687
|
if (missing.length === 0) return void 0;
|
|
1348
|
-
const
|
|
1688
|
+
const open = /^\s*---\r?\n/.exec(text);
|
|
1689
|
+
const script = (open && /^---\s*$/m.test(text.slice(open[0].length)) ? open : void 0) ?? /<script\b[^>]*>\r?\n?/.exec(text);
|
|
1349
1690
|
let at = 0;
|
|
1350
1691
|
if (script) at = script.index + script[0].length;
|
|
1351
1692
|
else {
|
|
@@ -1373,6 +1714,9 @@ var PlanBuilder = class {
|
|
|
1373
1714
|
touched = /* @__PURE__ */ new Set();
|
|
1374
1715
|
stateById;
|
|
1375
1716
|
config;
|
|
1717
|
+
/** Source values set by this plan, for the source records of targets set after them. */
|
|
1718
|
+
plannedSource = /* @__PURE__ */ new Map();
|
|
1719
|
+
draft;
|
|
1376
1720
|
constructor(options, state, answers) {
|
|
1377
1721
|
this.options = options;
|
|
1378
1722
|
this.state = state;
|
|
@@ -1439,7 +1783,9 @@ var PlanBuilder = class {
|
|
|
1439
1783
|
const sourceState = source && this.stateById.get(source.resource);
|
|
1440
1784
|
const namespace = namespaced ? this.keyId(key).namespace ?? this.config.namespace.default : sourceState?.resource.namespace;
|
|
1441
1785
|
const exists = (path) => resources.some((r) => r.resource.path === path);
|
|
1442
|
-
const
|
|
1786
|
+
const sibling = (r) => r.resource.locale === locale && r.resource.namespace !== void 0 && namespace !== void 0;
|
|
1787
|
+
const model = (sourceState && fits(sourceState) ? sourceState : void 0) ?? resources.find((s) => s.resource.locale === sourceLocale && fits(s)) ?? resources.find(fits) ?? resources.find(sibling);
|
|
1788
|
+
const resolved = this.options.resolveNew(locale, namespace, model?.resource);
|
|
1443
1789
|
if (resolved && !exists(resolved)) {
|
|
1444
1790
|
const format = this.options.registry.formatFor(resolved);
|
|
1445
1791
|
if (format?.serialize) {
|
|
@@ -1455,8 +1801,6 @@ var PlanBuilder = class {
|
|
|
1455
1801
|
return target;
|
|
1456
1802
|
}
|
|
1457
1803
|
}
|
|
1458
|
-
const sibling = (r) => r.resource.locale === locale && r.resource.namespace !== void 0 && namespace !== void 0;
|
|
1459
|
-
const model = (sourceState && fits(sourceState) ? sourceState : void 0) ?? resources.find((s) => s.resource.locale === sourceLocale && fits(s)) ?? resources.find(fits) ?? resources.find(sibling);
|
|
1460
1804
|
const format = model && this.options.registry.formatById(model.resource.format);
|
|
1461
1805
|
let path = model && mirrorPath(model.resource.path, model.resource.locale, locale);
|
|
1462
1806
|
if (model && path && namespace !== void 0 && model.resource.namespace !== void 0 && model.resource.namespace !== namespace) path = mirrorPath(path, model.resource.namespace, namespace);
|
|
@@ -1699,6 +2043,7 @@ var PlanBuilder = class {
|
|
|
1699
2043
|
const renamed = keys.includes(op.from) ? /* @__PURE__ */ new Set() : this.renameNode(op.from, op.to);
|
|
1700
2044
|
for (const [from, to] of pairs) {
|
|
1701
2045
|
this.moveKey(from, to, renamed);
|
|
2046
|
+
this.reviewDraft()?.move(from, to);
|
|
1702
2047
|
if (op.usages !== false) this.rewriteUsages(from, to);
|
|
1703
2048
|
}
|
|
1704
2049
|
if (op.usages === false) return;
|
|
@@ -1710,6 +2055,7 @@ var PlanBuilder = class {
|
|
|
1710
2055
|
if (keys.length === 0) throw errors.X18N4006({ key: op.key });
|
|
1711
2056
|
for (const key of keys) {
|
|
1712
2057
|
this.touched.add(key);
|
|
2058
|
+
this.reviewDraft()?.remove(key, op.locales ?? "all");
|
|
1713
2059
|
for (const [locale, list] of this.options.messages(key) ?? []) if (op.locales === void 0 || op.locales === "all" || op.locales.includes(locale)) list.forEach((m) => this.deleteMessage(m));
|
|
1714
2060
|
const used = this.options.usageMatches({ key }).filter((match) => !match.possible).length;
|
|
1715
2061
|
if (used > 0) this.warnings.push(`"${key}" still has ${used} usage${used === 1 ? "" : "s"}.`);
|
|
@@ -1729,13 +2075,75 @@ var PlanBuilder = class {
|
|
|
1729
2075
|
}
|
|
1730
2076
|
}
|
|
1731
2077
|
}
|
|
2078
|
+
/** The review draft, or undefined when reviews are off. */
|
|
2079
|
+
reviewDraft() {
|
|
2080
|
+
const reviews = this.state.reviews;
|
|
2081
|
+
if (reviews) this.draft ??= new ReviewDraft(reviews.data);
|
|
2082
|
+
return this.draft;
|
|
2083
|
+
}
|
|
2084
|
+
requireReviews() {
|
|
2085
|
+
const draft = this.reviewDraft();
|
|
2086
|
+
if (!draft) throw errors.X18N5003({ reason: this.state.reviewProblem ?? "reviews are off" });
|
|
2087
|
+
return draft;
|
|
2088
|
+
}
|
|
2089
|
+
/** Records the source value a target was written against, once the project keeps a review file. */
|
|
2090
|
+
recordSource(key, locale, force = false) {
|
|
2091
|
+
if (locale === this.state.sourceLocale || !force && this.state.reviews?.text === void 0) return;
|
|
2092
|
+
const source = this.plannedSource.has(key) ? this.plannedSource.get(key) : this.options.message(key, this.state.sourceLocale)?.value;
|
|
2093
|
+
if (source !== void 0) this.reviewDraft()?.record(key, locale, sourceHash(source));
|
|
2094
|
+
}
|
|
2095
|
+
review(op) {
|
|
2096
|
+
const draft = this.requireReviews();
|
|
2097
|
+
if (!this.options.messages(op.key)) throw errors.X18N4006({ key: op.key });
|
|
2098
|
+
if (!this.state.locales.includes(op.locale)) throw errors.X18N4008({ reason: `unknown locale "${op.locale}"` });
|
|
2099
|
+
if (op.review === "suggestion" && op.suggestion === void 0) throw errors.X18N4008({ reason: "a suggestion needs a value" });
|
|
2100
|
+
const current = this.options.message(op.key, op.locale)?.value;
|
|
2101
|
+
this.touched.add(op.key);
|
|
2102
|
+
draft.add(op.key, op.locale, {
|
|
2103
|
+
id: op.id,
|
|
2104
|
+
type: op.review,
|
|
2105
|
+
...op.author && { author: op.author },
|
|
2106
|
+
time: op.time,
|
|
2107
|
+
...op.text !== void 0 && { text: op.text },
|
|
2108
|
+
...op.review === "suggestion" && { suggestion: op.suggestion },
|
|
2109
|
+
...op.review === "approve" && current !== void 0 && { value: valueText(current) }
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
applySuggestion(id) {
|
|
2113
|
+
const draft = this.requireReviews();
|
|
2114
|
+
const found = findReview(draft.data, id);
|
|
2115
|
+
if (!found) throw errors.X18N5004({ id });
|
|
2116
|
+
const { key, locale, comment } = found;
|
|
2117
|
+
if (comment.type !== "suggestion" || comment.suggestion === void 0 || comment.resolved) throw errors.X18N5005({ id });
|
|
2118
|
+
this.setValue(key, locale, comment.suggestion);
|
|
2119
|
+
this.recordSource(key, locale, true);
|
|
2120
|
+
draft.resolve(id);
|
|
2121
|
+
}
|
|
2122
|
+
resolveReview(id) {
|
|
2123
|
+
const draft = this.requireReviews();
|
|
2124
|
+
const found = findReview(draft.data, id);
|
|
2125
|
+
if (!found) throw errors.X18N5004({ id });
|
|
2126
|
+
this.touched.add(found.key);
|
|
2127
|
+
draft.resolve(id);
|
|
2128
|
+
}
|
|
2129
|
+
markFresh(op) {
|
|
2130
|
+
this.requireReviews();
|
|
2131
|
+
if (this.options.message(op.key, this.state.sourceLocale) === void 0) throw errors.X18N4006({ key: op.key });
|
|
2132
|
+
const locales = op.locales ?? this.state.locales.filter((locale) => this.options.message(op.key, locale) !== void 0);
|
|
2133
|
+
this.touched.add(op.key);
|
|
2134
|
+
for (const locale of locales) this.recordSource(op.key, locale, true);
|
|
2135
|
+
}
|
|
2136
|
+
set(op) {
|
|
2137
|
+
this.setValue(op.key, op.locale, op.value);
|
|
2138
|
+
if (op.locale === this.state.sourceLocale) this.plannedSource.set(op.key, op.value);
|
|
2139
|
+
else if (op.value !== "") this.recordSource(op.key, op.locale);
|
|
2140
|
+
if (!this.config.write.keepFilled) return;
|
|
2141
|
+
for (const locale of this.state.locales) if (locale !== op.locale && !this.state.fallbacks[locale] && !this.options.message(op.key, locale)) this.setValue(op.key, locale, "");
|
|
2142
|
+
}
|
|
1732
2143
|
run(ops) {
|
|
1733
2144
|
for (const op of ops) switch (op.type) {
|
|
1734
2145
|
case "set":
|
|
1735
|
-
this.
|
|
1736
|
-
if (this.config.write.keepFilled) {
|
|
1737
|
-
for (const locale of this.state.locales) if (locale !== op.locale && !this.state.fallbacks[locale] && !this.options.message(op.key, locale)) this.setValue(op.key, locale, "");
|
|
1738
|
-
}
|
|
2146
|
+
this.set(op);
|
|
1739
2147
|
break;
|
|
1740
2148
|
case "delete":
|
|
1741
2149
|
this.delete(op);
|
|
@@ -1747,7 +2155,19 @@ var PlanBuilder = class {
|
|
|
1747
2155
|
case "extract":
|
|
1748
2156
|
this.extract(op.items);
|
|
1749
2157
|
break;
|
|
1750
|
-
case "format":
|
|
2158
|
+
case "format":
|
|
2159
|
+
for (const s of this.state.resources) if ((!op.paths || op.paths.includes(s.resource.path)) && !s.resource.readonly && !s.error) this.workFor(this.targetOf(s)).formatOnly = true;
|
|
2160
|
+
break;
|
|
2161
|
+
case "review":
|
|
2162
|
+
this.review(op);
|
|
2163
|
+
break;
|
|
2164
|
+
case "resolve-review":
|
|
2165
|
+
this.resolveReview(op.id);
|
|
2166
|
+
break;
|
|
2167
|
+
case "apply-suggestion":
|
|
2168
|
+
this.applySuggestion(op.id);
|
|
2169
|
+
break;
|
|
2170
|
+
case "mark-fresh": this.markFresh(op);
|
|
1751
2171
|
}
|
|
1752
2172
|
}
|
|
1753
2173
|
/** ---- Changes ---- */
|
|
@@ -1856,16 +2276,52 @@ var PlanBuilder = class {
|
|
|
1856
2276
|
});
|
|
1857
2277
|
texts.set(path, text);
|
|
1858
2278
|
}
|
|
2279
|
+
const review = this.reviewChange();
|
|
2280
|
+
if (review) {
|
|
2281
|
+
changes.push(review);
|
|
2282
|
+
texts.set(review.path, this.state.reviews?.text);
|
|
2283
|
+
}
|
|
1859
2284
|
return {
|
|
1860
2285
|
changes,
|
|
1861
2286
|
texts
|
|
1862
2287
|
};
|
|
1863
2288
|
}
|
|
2289
|
+
reviewChange() {
|
|
2290
|
+
const reviews = this.state.reviews;
|
|
2291
|
+
if (!reviews || !this.draft?.changed) return void 0;
|
|
2292
|
+
const next = reviews.store.serialize(sortReviews(this.draft.data));
|
|
2293
|
+
if (reviews.text === void 0) return {
|
|
2294
|
+
type: "create",
|
|
2295
|
+
path: reviews.file,
|
|
2296
|
+
content: next
|
|
2297
|
+
};
|
|
2298
|
+
return next === reviews.text ? void 0 : {
|
|
2299
|
+
type: "edit",
|
|
2300
|
+
path: reviews.file,
|
|
2301
|
+
edits: diffText(reviews.text, next)
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
1864
2304
|
};
|
|
1865
2305
|
function createPlanner(options) {
|
|
1866
2306
|
const texts = /* @__PURE__ */ new Map();
|
|
1867
|
-
|
|
2307
|
+
/** Gives new reviews an id and a time, so planning again (after decisions) writes the same comments. */
|
|
2308
|
+
function withReviewIds(ops, data) {
|
|
2309
|
+
const used = /* @__PURE__ */ new Set();
|
|
2310
|
+
const time = (/* @__PURE__ */ new Date()).toISOString();
|
|
2311
|
+
return ops.map((op) => {
|
|
2312
|
+
if (op.type !== "review" || op.id !== void 0 && op.time !== void 0) return op;
|
|
2313
|
+
const id = op.id ?? reviewId((id) => used.has(id) || findReview(data, id) !== void 0);
|
|
2314
|
+
used.add(id);
|
|
2315
|
+
return {
|
|
2316
|
+
...op,
|
|
2317
|
+
id,
|
|
2318
|
+
time: op.time ?? time
|
|
2319
|
+
};
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
function planWith(input, answers) {
|
|
1868
2323
|
const state = options.state();
|
|
2324
|
+
const ops = withReviewIds(input, state.reviews?.data ?? noReviews);
|
|
1869
2325
|
const builder = new PlanBuilder(options, state, answers);
|
|
1870
2326
|
builder.run(ops);
|
|
1871
2327
|
const result = builder.changes();
|
|
@@ -1965,7 +2421,8 @@ function buildPrompt(project, kind, to, options, extra) {
|
|
|
1965
2421
|
const from = project.sourceLocale;
|
|
1966
2422
|
const match = typeof options.keys === "string" ? keyMatcher([options.keys]) : void 0;
|
|
1967
2423
|
const list = Array.isArray(options.keys) ? new Set(options.keys) : void 0;
|
|
1968
|
-
const
|
|
2424
|
+
const outdated = new Set(project.analyze().coverage.find((c) => c.locale === to)?.outdated);
|
|
2425
|
+
const stale = (key) => outdated.has(key);
|
|
1969
2426
|
const candidates = project.keys({ namespace: options.namespace }).filter((key) => {
|
|
1970
2427
|
if (match && !match(key)) return false;
|
|
1971
2428
|
if (list && !list.has(key)) return false;
|
|
@@ -2020,7 +2477,11 @@ function buildPrompt(project, kind, to, options, extra) {
|
|
|
2020
2477
|
`Write a JSON file \`{ "${to}": { "<key>": "<translation>" } }\` and run \`x18n apply <file> --json\`, or run \`x18n set <key> --locale ${to} --value <text>\` per key.`,
|
|
2021
2478
|
`Then run \`x18n check --only placeholder-mismatch,invalid-syntax --locale ${to} --json\` and fix the reported keys.`,
|
|
2022
2479
|
nextCursor ? `${candidates.length - end} more keys remain: \`x18n prompt translate --to ${to} --cursor ${nextCursor}\`.` : `To find more work: \`x18n keys --missing ${to} --json --limit 50\`.`
|
|
2023
|
-
] : [`Fix wrong translations with \`x18n set <key> --locale ${to} --value <text>\`, then list what you changed and why.`, `Then run \`x18n check --only placeholder-mismatch,invalid-syntax --locale ${to} --json\`.`]
|
|
2480
|
+
] : project.reviewFile === void 0 ? [`Fix wrong translations with \`x18n set <key> --locale ${to} --value <text>\`, then list what you changed and why.`, `Then run \`x18n check --only placeholder-mismatch,invalid-syntax --locale ${to} --json\`.`] : [
|
|
2481
|
+
`Leave fixes for a human to accept: \`x18n review suggest <key> --locale ${to} --value <text> --text <why>\`. Keep every placeholder in the value.`,
|
|
2482
|
+
`For a problem without a clear fix, run \`x18n review request-change <key> --locale ${to} --text <why>\`.`,
|
|
2483
|
+
`Then list what you reviewed; \`x18n review list --locale ${to} --open\` shows the open reviews.`
|
|
2484
|
+
];
|
|
2024
2485
|
return {
|
|
2025
2486
|
kind,
|
|
2026
2487
|
project: project.name,
|
|
@@ -2102,6 +2563,10 @@ function createProject(options) {
|
|
|
2102
2563
|
let usageEligible = () => false;
|
|
2103
2564
|
let resourceIndex;
|
|
2104
2565
|
let resolved = /* @__PURE__ */ new WeakMap();
|
|
2566
|
+
const reviewFile = config.review.file;
|
|
2567
|
+
let reviewText;
|
|
2568
|
+
let reviewData = noReviews;
|
|
2569
|
+
let reviewError;
|
|
2105
2570
|
const normalizers = registry.plugins.flatMap((plugin) => plugin.normalizeKey ? [plugin.normalizeKey] : []);
|
|
2106
2571
|
const transforms = registry.plugins.flatMap((plugin) => plugin.transformResource ? [plugin.transformResource] : []);
|
|
2107
2572
|
const localeInfoHooks = registry.plugins.flatMap((plugin) => plugin.localeInfo ? [plugin.localeInfo] : []);
|
|
@@ -2138,6 +2603,44 @@ function createProject(options) {
|
|
|
2138
2603
|
const form = formOf(key);
|
|
2139
2604
|
return !form || categoriesOf(localeInfo(locale), form.type).includes(form.category);
|
|
2140
2605
|
}
|
|
2606
|
+
const reviewStore = config.review.enabled ? registry.reviewStores.at(-1) : void 0;
|
|
2607
|
+
async function loadReviews() {
|
|
2608
|
+
if (!reviewStore) return;
|
|
2609
|
+
reviewText = await host.readFile(reviewFile);
|
|
2610
|
+
reviewError = void 0;
|
|
2611
|
+
reviewData = noReviews;
|
|
2612
|
+
if (reviewText === void 0) return;
|
|
2613
|
+
try {
|
|
2614
|
+
reviewData = reviewStore.parse(reviewText);
|
|
2615
|
+
} catch (error) {
|
|
2616
|
+
reviewError = errors.X18N5001({
|
|
2617
|
+
path: reviewFile,
|
|
2618
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
2619
|
+
cause: error
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
/** Reviews the planner may change: none while the file cannot be read, so a write never drops what is in it. */
|
|
2624
|
+
function reviewSource() {
|
|
2625
|
+
return reviewStore && !reviewError ? {
|
|
2626
|
+
store: reviewStore,
|
|
2627
|
+
file: reviewFile,
|
|
2628
|
+
text: reviewText,
|
|
2629
|
+
data: reviewData
|
|
2630
|
+
} : void 0;
|
|
2631
|
+
}
|
|
2632
|
+
function reviewProblem() {
|
|
2633
|
+
return reviewError?.message ?? (config.review.enabled ? "no review store is registered" : "`review.enabled` is false");
|
|
2634
|
+
}
|
|
2635
|
+
function reviews(filter = {}) {
|
|
2636
|
+
return listThreads(reviewData, filter, locales, (key, locale) => message(key, locale)?.value);
|
|
2637
|
+
}
|
|
2638
|
+
/** A target recorded as written against the current source value. */
|
|
2639
|
+
function fresh(key, locale) {
|
|
2640
|
+
const recorded = reviewData.sources[key]?.[locale];
|
|
2641
|
+
const source = message(key, sourceLocale)?.value;
|
|
2642
|
+
return recorded !== void 0 && source !== void 0 && recorded === sourceHash(source);
|
|
2643
|
+
}
|
|
2141
2644
|
async function discover() {
|
|
2142
2645
|
const ctx = {
|
|
2143
2646
|
host,
|
|
@@ -2265,6 +2768,22 @@ function createProject(options) {
|
|
|
2265
2768
|
if (state.error) return void 0;
|
|
2266
2769
|
return new Map([...walkLeaves(state.tree)].map((leaf) => [keyFor(state.resource, leaf.path), leaf.value]));
|
|
2267
2770
|
}
|
|
2771
|
+
/** A path for a new resource, from the locale finders. */
|
|
2772
|
+
function newResourcePath(locale, namespace, model) {
|
|
2773
|
+
const ctx = {
|
|
2774
|
+
host,
|
|
2775
|
+
config,
|
|
2776
|
+
extensions: [...new Set(registry.formats.flatMap((format) => format.extensions))]
|
|
2777
|
+
};
|
|
2778
|
+
for (const finder of registry.finders) {
|
|
2779
|
+
const path = finder.resolveNew?.({
|
|
2780
|
+
locale,
|
|
2781
|
+
namespace,
|
|
2782
|
+
model
|
|
2783
|
+
}, ctx);
|
|
2784
|
+
if (path) return path;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2268
2787
|
async function loadResources(candidates, reread) {
|
|
2269
2788
|
const next = /* @__PURE__ */ new Map();
|
|
2270
2789
|
const paths = [...new Set(candidates.map((c) => c.path))];
|
|
@@ -2276,6 +2795,7 @@ function createProject(options) {
|
|
|
2276
2795
|
}
|
|
2277
2796
|
}));
|
|
2278
2797
|
namespaced = config.namespace.separator !== "." && candidates.some((c) => c.namespace !== void 0);
|
|
2798
|
+
await Promise.all(candidates.map((candidate) => registry.loadFormatFor(candidate.path, candidate.format)));
|
|
2279
2799
|
for (const candidate of candidates) {
|
|
2280
2800
|
const text = fileTexts.get(candidate.path);
|
|
2281
2801
|
if (text === void 0) continue;
|
|
@@ -2296,6 +2816,11 @@ function createProject(options) {
|
|
|
2296
2816
|
resolver = createUsageResolver(config, namespaced, normalizers);
|
|
2297
2817
|
resolved = /* @__PURE__ */ new WeakMap();
|
|
2298
2818
|
resourceIndex = void 0;
|
|
2819
|
+
const namespaces = /* @__PURE__ */ new Set([void 0, ...[...resourceStates.values()].map((state) => state.resource.namespace)]);
|
|
2820
|
+
await Promise.all([...namespaces].map((namespace) => {
|
|
2821
|
+
const path = newResourcePath(sourceLocale, namespace, void 0);
|
|
2822
|
+
return path ? registry.loadFormatFor(path) : void 0;
|
|
2823
|
+
}));
|
|
2299
2824
|
}
|
|
2300
2825
|
async function scanOne(file) {
|
|
2301
2826
|
const usages = [];
|
|
@@ -2303,7 +2828,7 @@ function createProject(options) {
|
|
|
2303
2828
|
const keySlots = [];
|
|
2304
2829
|
const hardStrings = [];
|
|
2305
2830
|
const calls = config.usage.calls.filter((spec) => ![spec.callee].flat().some((name) => config.usage.ignoreCallees.includes(name)));
|
|
2306
|
-
for (const scanner of registry.
|
|
2831
|
+
for (const scanner of await registry.loadScannersFor(file.path)) {
|
|
2307
2832
|
const result = await scanner.scan(file, {
|
|
2308
2833
|
config,
|
|
2309
2834
|
calls
|
|
@@ -2470,12 +2995,13 @@ function createProject(options) {
|
|
|
2470
2995
|
};
|
|
2471
2996
|
}
|
|
2472
2997
|
function buildIndex() {
|
|
2473
|
-
resourceIndex ??= buildResourceIndex();
|
|
2998
|
+
const { messages, parents, derived } = resourceIndex ??= buildResourceIndex();
|
|
2999
|
+
const has = (key) => messages.has(key) || parents.has(key) || derived.has(key);
|
|
2474
3000
|
const usages = [];
|
|
2475
3001
|
for (const [path, state] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
2476
3002
|
let list = resolved.get(state);
|
|
2477
3003
|
if (!list) {
|
|
2478
|
-
list = state.result.usages.map((candidate) => resolver.resolve(candidate, state.result.scopes ?? [], path, "scanner"));
|
|
3004
|
+
list = state.result.usages.map((candidate) => resolver.resolve(candidate, state.result.scopes ?? [], path, "scanner", has));
|
|
2479
3005
|
resolved.set(state, list);
|
|
2480
3006
|
}
|
|
2481
3007
|
usages.push(...list);
|
|
@@ -2571,7 +3097,10 @@ function createProject(options) {
|
|
|
2571
3097
|
error: s.error,
|
|
2572
3098
|
path
|
|
2573
3099
|
}] : []),
|
|
2574
|
-
discoveryErrors,
|
|
3100
|
+
discoveryErrors: [...discoveryErrors, ...reviewError ? [{
|
|
3101
|
+
error: reviewError,
|
|
3102
|
+
path: reviewFile
|
|
3103
|
+
}] : []],
|
|
2575
3104
|
messages: idx().messages,
|
|
2576
3105
|
parents: idx().parents,
|
|
2577
3106
|
usages: idx().usages,
|
|
@@ -2579,7 +3108,9 @@ function createProject(options) {
|
|
|
2579
3108
|
prefixUsages: idx().prefixUsages,
|
|
2580
3109
|
derived: idx().derived,
|
|
2581
3110
|
files,
|
|
2582
|
-
history: history.get
|
|
3111
|
+
history: history.get,
|
|
3112
|
+
fresh,
|
|
3113
|
+
reviews
|
|
2583
3114
|
}),
|
|
2584
3115
|
basesOf,
|
|
2585
3116
|
formOf,
|
|
@@ -2605,6 +3136,29 @@ function createProject(options) {
|
|
|
2605
3136
|
}
|
|
2606
3137
|
return list.sort();
|
|
2607
3138
|
}
|
|
3139
|
+
function search(query, options = {}) {
|
|
3140
|
+
if (!query) return [];
|
|
3141
|
+
const lower = query.toLowerCase();
|
|
3142
|
+
const score = (value) => !options.exact || value.toLowerCase().includes(lower) ? fuzzyScore(query, value) : -1;
|
|
3143
|
+
const found = [];
|
|
3144
|
+
for (const key of idx().messages.keys()) {
|
|
3145
|
+
let best;
|
|
3146
|
+
for (const locale of locales) {
|
|
3147
|
+
const leaf = message(key, locale)?.value;
|
|
3148
|
+
if (leaf === void 0 || leaf === null) continue;
|
|
3149
|
+
const value = String(leaf);
|
|
3150
|
+
const s = score(value);
|
|
3151
|
+
if (s >= 0 && (!best || s > best.score)) best = {
|
|
3152
|
+
key,
|
|
3153
|
+
locale,
|
|
3154
|
+
value,
|
|
3155
|
+
score: s
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
if (best) found.push(best);
|
|
3159
|
+
}
|
|
3160
|
+
return found.sort((a, b) => b.score - a.score || a.value.length - b.value.length || a.key.localeCompare(b.key));
|
|
3161
|
+
}
|
|
2608
3162
|
function get(key) {
|
|
2609
3163
|
const { messages, keyPaths } = idx();
|
|
2610
3164
|
const byLocale = messages.get(key);
|
|
@@ -2618,7 +3172,8 @@ function createProject(options) {
|
|
|
2618
3172
|
messages: list,
|
|
2619
3173
|
missing: locales.filter((locale) => !byLocale.has(locale) && needs(key, locale)),
|
|
2620
3174
|
usages: usageMatches({ key }),
|
|
2621
|
-
diagnostics: analyzer.analyze().diagnostics.filter((d) => d.key === key)
|
|
3175
|
+
diagnostics: analyzer.analyze().diagnostics.filter((d) => d.key === key),
|
|
3176
|
+
reviews: reviews({ key })
|
|
2622
3177
|
};
|
|
2623
3178
|
}
|
|
2624
3179
|
function keyAt(path, offset) {
|
|
@@ -2672,6 +3227,10 @@ function createProject(options) {
|
|
|
2672
3227
|
conflicts: registry.conflicts,
|
|
2673
3228
|
errors: [
|
|
2674
3229
|
...discoveryErrors,
|
|
3230
|
+
...reviewError ? [{
|
|
3231
|
+
error: reviewError,
|
|
3232
|
+
path: reviewFile
|
|
3233
|
+
}] : [],
|
|
2675
3234
|
...resources.flatMap((state) => state.error ? [{
|
|
2676
3235
|
error: state.error,
|
|
2677
3236
|
path: state.resource.path
|
|
@@ -2701,15 +3260,24 @@ function createProject(options) {
|
|
|
2701
3260
|
collect(idx());
|
|
2702
3261
|
return [...out];
|
|
2703
3262
|
}
|
|
3263
|
+
/** Reloads the review file and returns the keys whose reviews or records it had or has. */
|
|
3264
|
+
async function reloadReviews() {
|
|
3265
|
+
const keysOf = (data) => [...Object.keys(data.threads), ...Object.keys(data.sources)];
|
|
3266
|
+
const old = keysOf(reviewData);
|
|
3267
|
+
await loadReviews();
|
|
3268
|
+
return [...old, ...keysOf(reviewData)];
|
|
3269
|
+
}
|
|
2704
3270
|
let current = Promise.resolve();
|
|
2705
3271
|
async function update(events) {
|
|
2706
3272
|
const run = current.then(async () => {
|
|
2707
3273
|
if (events.length === 0) return void 0;
|
|
2708
3274
|
const before = idx();
|
|
2709
3275
|
const paths = new Set(events.map((event) => event.path));
|
|
3276
|
+
const reviewKeys = paths.has(reviewFile) ? await reloadReviews() : [];
|
|
2710
3277
|
const resourcePaths = new Set([...resourceStates.values()].map((state) => state.resource.path));
|
|
2711
|
-
const maybeResource = (path) => !resourcePaths.has(path) && registry.
|
|
2712
|
-
const
|
|
3278
|
+
const maybeResource = (path) => path !== reviewFile && !resourcePaths.has(path) && registry.formatIdFor(path) !== void 0 && !usageEligible(path);
|
|
3279
|
+
const blockPaths = new Set([...resourceStates.values()].filter((state) => state.resource.block).map((state) => state.resource.path));
|
|
3280
|
+
const structural = events.some((event) => event.type !== "change" || maybeResource(event.path) || blockPaths.has(event.path));
|
|
2713
3281
|
if (structural || [...paths].some((path) => resourcePaths.has(path))) await loadResources(structural ? await discover() : [...resourceStates.values()].map((state) => ({
|
|
2714
3282
|
...state.resource,
|
|
2715
3283
|
format: state.resource.format === "unknown" ? void 0 : state.resource.format
|
|
@@ -2721,7 +3289,7 @@ function createProject(options) {
|
|
|
2721
3289
|
const change = {
|
|
2722
3290
|
revision,
|
|
2723
3291
|
paths: [...paths],
|
|
2724
|
-
keys: touchedKeys(paths, before)
|
|
3292
|
+
keys: [.../* @__PURE__ */ new Set([...touchedKeys(paths, before), ...reviewKeys])]
|
|
2725
3293
|
};
|
|
2726
3294
|
emit(change);
|
|
2727
3295
|
return change;
|
|
@@ -2740,7 +3308,9 @@ function createProject(options) {
|
|
|
2740
3308
|
fallbacks,
|
|
2741
3309
|
namespaced,
|
|
2742
3310
|
resources: [...resourceStates.values()],
|
|
2743
|
-
fileTexts
|
|
3311
|
+
fileTexts,
|
|
3312
|
+
reviews: reviewSource(),
|
|
3313
|
+
reviewProblem: reviewProblem()
|
|
2744
3314
|
}),
|
|
2745
3315
|
message: (key, locale) => message(key, locale),
|
|
2746
3316
|
messages: (key) => idx().messages.get(key),
|
|
@@ -2750,24 +3320,12 @@ function createProject(options) {
|
|
|
2750
3320
|
text: (path) => fileTexts.get(path) ?? files.get(path)?.text,
|
|
2751
3321
|
basesOf,
|
|
2752
3322
|
toStored: (target, path) => storedPathOf(target)(path),
|
|
2753
|
-
resolveNew
|
|
2754
|
-
const ctx = {
|
|
2755
|
-
host,
|
|
2756
|
-
config,
|
|
2757
|
-
extensions: [...new Set(registry.formats.flatMap((format) => format.extensions))]
|
|
2758
|
-
};
|
|
2759
|
-
for (const finder of registry.finders) {
|
|
2760
|
-
const path = finder.resolveNew?.({
|
|
2761
|
-
locale,
|
|
2762
|
-
namespace
|
|
2763
|
-
}, ctx);
|
|
2764
|
-
if (path) return path;
|
|
2765
|
-
}
|
|
2766
|
-
},
|
|
3323
|
+
resolveNew: newResourcePath,
|
|
2767
3324
|
update
|
|
2768
3325
|
});
|
|
2769
3326
|
const ready = (async () => {
|
|
2770
|
-
|
|
3327
|
+
const candidates = await discover();
|
|
3328
|
+
await Promise.all([loadResources(candidates, "all"), loadReviews()]);
|
|
2771
3329
|
await scanAll();
|
|
2772
3330
|
revision++;
|
|
2773
3331
|
})();
|
|
@@ -2796,6 +3354,7 @@ function createProject(options) {
|
|
|
2796
3354
|
placeholderSyntaxes: registry.placeholders,
|
|
2797
3355
|
extractTemplates: [...(config.extract.templates ?? []).map((template) => typeof template === "string" ? { template } : template), ...registry.extractors.flatMap((extractor) => extractor.templates)],
|
|
2798
3356
|
keys,
|
|
3357
|
+
search,
|
|
2799
3358
|
get,
|
|
2800
3359
|
message,
|
|
2801
3360
|
parseKey(input) {
|
|
@@ -2838,13 +3397,18 @@ function createProject(options) {
|
|
|
2838
3397
|
},
|
|
2839
3398
|
history: history.get,
|
|
2840
3399
|
valueAt: history.valueAt,
|
|
3400
|
+
reviews,
|
|
3401
|
+
reviewFile: reviewStore && reviewFile,
|
|
3402
|
+
async reviewAuthor() {
|
|
3403
|
+
return config.review.author ?? await host.identity?.();
|
|
3404
|
+
},
|
|
2841
3405
|
prompt: (kind, locale, promptOptions = {}) => buildPrompt(project, kind, locale, promptOptions, {
|
|
2842
3406
|
config,
|
|
2843
3407
|
hooks: registry.plugins.flatMap((plugin) => plugin.translatePrompt ? [plugin.translatePrompt] : [])
|
|
2844
3408
|
}),
|
|
2845
3409
|
explain,
|
|
2846
3410
|
localeInfo,
|
|
2847
|
-
text: (path) => fileTexts.get(path) ?? files.get(path)?.text,
|
|
3411
|
+
text: (path) => fileTexts.get(path) ?? files.get(path)?.text ?? (path === reviewFile ? reviewText : void 0),
|
|
2848
3412
|
owns: (path) => fileTexts.has(path) || files.has(path) || usageEligible(path),
|
|
2849
3413
|
plan: (ops) => planner.plan(ops),
|
|
2850
3414
|
apply: (plan, applyOptions) => planner.apply(plan, applyOptions?.answers ?? {}),
|
|
@@ -2894,6 +3458,8 @@ function defaultDraft() {
|
|
|
2894
3458
|
namespace: {
|
|
2895
3459
|
separator: ".",
|
|
2896
3460
|
default: void 0,
|
|
3461
|
+
fallback: [],
|
|
3462
|
+
byFile: {},
|
|
2897
3463
|
fromPath: "dot"
|
|
2898
3464
|
},
|
|
2899
3465
|
usage: {
|
|
@@ -2916,6 +3482,11 @@ function defaultDraft() {
|
|
|
2916
3482
|
ignoreKeys: [],
|
|
2917
3483
|
glossary: [],
|
|
2918
3484
|
history: { providers: [] },
|
|
3485
|
+
review: {
|
|
3486
|
+
enabled: true,
|
|
3487
|
+
file: ".x18n/reviews.yml",
|
|
3488
|
+
author: void 0
|
|
3489
|
+
},
|
|
2919
3490
|
pluginConfigs: {}
|
|
2920
3491
|
};
|
|
2921
3492
|
}
|
|
@@ -2977,6 +3548,11 @@ function applyLayer(draft, layer, origin, origins) {
|
|
|
2977
3548
|
draft.namespace = {
|
|
2978
3549
|
separator: layer.namespace.separator ?? draft.namespace.separator,
|
|
2979
3550
|
default: layer.namespace.default ?? draft.namespace.default,
|
|
3551
|
+
fallback: layer.namespace.fallback ?? draft.namespace.fallback,
|
|
3552
|
+
byFile: {
|
|
3553
|
+
...draft.namespace.byFile,
|
|
3554
|
+
...layer.namespace.byFile
|
|
3555
|
+
},
|
|
2980
3556
|
fromPath: layer.namespace.fromPath ?? draft.namespace.fromPath
|
|
2981
3557
|
};
|
|
2982
3558
|
mark("namespace");
|
|
@@ -3038,6 +3614,14 @@ function applyLayer(draft, layer, origin, origins) {
|
|
|
3038
3614
|
};
|
|
3039
3615
|
mark("history");
|
|
3040
3616
|
}
|
|
3617
|
+
if (layer.review !== void 0) {
|
|
3618
|
+
draft.review = {
|
|
3619
|
+
enabled: layer.review.enabled ?? draft.review.enabled,
|
|
3620
|
+
file: layer.review.file ?? draft.review.file,
|
|
3621
|
+
author: layer.review.author ?? draft.review.author
|
|
3622
|
+
};
|
|
3623
|
+
mark("review");
|
|
3624
|
+
}
|
|
3041
3625
|
if (layer.pluginConfigs !== void 0) {
|
|
3042
3626
|
draft.pluginConfigs = {
|
|
3043
3627
|
...draft.pluginConfigs,
|
|
@@ -3081,8 +3665,8 @@ function createLoader(options) {
|
|
|
3081
3665
|
if (option === false || option === void 0) return void 0;
|
|
3082
3666
|
if (typeof option === "object" && !Array.isArray(option)) return option;
|
|
3083
3667
|
const [specifier, pluginOptions] = typeof option === "string" ? [option, void 0] : [option[0], option[1]];
|
|
3084
|
-
const
|
|
3085
|
-
if (
|
|
3668
|
+
const entry = options.builtins.presets[specifier];
|
|
3669
|
+
if (entry) return (await entry.load()).default(pluginOptions);
|
|
3086
3670
|
if (!options.loadPlugin) throw errors.X18N1003({ preset: specifier });
|
|
3087
3671
|
return options.loadPlugin(specifier, pluginOptions);
|
|
3088
3672
|
};
|
|
@@ -3118,7 +3702,17 @@ async function expandPresets(presets, userPlugins, listed, detectCtx) {
|
|
|
3118
3702
|
name: preset.name,
|
|
3119
3703
|
reason: "after-exclusive"
|
|
3120
3704
|
});
|
|
3121
|
-
else
|
|
3705
|
+
else if ("entry" in preset) {
|
|
3706
|
+
const applies = listed || (preset.entry.detect ? await preset.entry.detect(detectCtx) : false);
|
|
3707
|
+
explanations.push({
|
|
3708
|
+
name: preset.name,
|
|
3709
|
+
reason: applies ? listed ? "listed" : "detected" : "not-detected"
|
|
3710
|
+
});
|
|
3711
|
+
if (!applies) continue;
|
|
3712
|
+
const loaded = (await preset.entry.load()).default();
|
|
3713
|
+
for (const child of loaded.plugins) await flatten(child, false);
|
|
3714
|
+
stopped = preset.entry.exclusive === true || loaded.exclusive === true;
|
|
3715
|
+
} else stopped = await flatten(preset, listed);
|
|
3122
3716
|
for (const plugin of userPlugins) await flatten(plugin, true);
|
|
3123
3717
|
return {
|
|
3124
3718
|
plugins,
|
|
@@ -3141,7 +3735,10 @@ async function resolveConfig(options) {
|
|
|
3141
3735
|
const userPlugins = await loadAll(load, [...options.parent?.plugins ?? [], ...user.plugins ?? []]);
|
|
3142
3736
|
const presetOption = user.presets ?? options.parent?.presets ?? "auto";
|
|
3143
3737
|
const auto = presetOption === "auto";
|
|
3144
|
-
const presets = auto ? Object.
|
|
3738
|
+
const presets = auto ? Object.entries(builtins.presets).map(([name, entry]) => ({
|
|
3739
|
+
name,
|
|
3740
|
+
entry
|
|
3741
|
+
})) : (await loadAll(load, presetOption)).map((item) => isPreset(item) ? item : {
|
|
3145
3742
|
name: item.name,
|
|
3146
3743
|
plugins: [item]
|
|
3147
3744
|
});
|
|
@@ -3182,4 +3779,4 @@ async function resolveConfig(options) {
|
|
|
3182
3779
|
};
|
|
3183
3780
|
}
|
|
3184
3781
|
//#endregion
|
|
3185
|
-
export { ConfigSchema, PluginFileSchema, applyMessageOps, applyTextEdits, buildTree, contractVersion, createLineIndex, createMemoryHost, createOverlayHost, createPlaceholderComparer, createProject, createRegistry, defineConfig, definePlugin, definePreset, diffText, envelope, errors, extensionOf, globToRegExp, hashText, isJsonObject, isKeyUnder, isMessageLeaf, isMessageTree, isPreset, isX18nError, joinKey, keyMatcher, localeSpellingKey, parseLocaleInfo, parseMessageBatch, pathId, pathMatcher, placeholderProblems, rangeContains, renderPrompt, replaceKeyPrefix, resolveConfig, sortTree, splitKey, toErrorInfo, toWireDiagnostic, toWireRange, validate, walkLeaves };
|
|
3782
|
+
export { ConfigSchema, PluginFileSchema, ReviewFileSchema, applyMessageOps, applyTextEdits, buildTree, contractVersion, createLibraryPlugin, createLineIndex, createMemoryHost, createOverlayHost, createPlaceholderComparer, createProject, createRegistry, defineConfig, definePlugin, definePreset, diffText, envelope, errors, extensionOf, fuzzyScore, globToRegExp, hashText, isJsonObject, isKeyUnder, isMessageLeaf, isMessageTree, isPreset, isX18nError, joinKey, keyMatcher, localeSpellingKey, parseLocaleInfo, parseMessageBatch, pathId, pathMatcher, placeholderProblems, rangeContains, renderExplanation, renderPrompt, replaceKeyPrefix, resolveConfig, sortTree, splitKey, toErrorInfo, toWireDiagnostic, toWireRange, validate, walkLeaves };
|