@x18n/core 0.1.9 → 0.1.10
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 +46 -2
- package/dist/index.mjs +211 -90
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -561,6 +561,8 @@ interface CheckContext {
|
|
|
561
561
|
readonly usages: () => Iterable<Usage>;
|
|
562
562
|
readonly coverage: () => readonly LocaleCoverage[];
|
|
563
563
|
readonly unused: () => readonly KeyString[];
|
|
564
|
+
/** Fully dynamic usages in a scope (`keyPrefix`, namespace) that may reach the key. `planUnused` keeps a key they reach. */
|
|
565
|
+
readonly dynamicUsages: (key: KeyString) => readonly Usage[];
|
|
564
566
|
/** Usages whose key is not defined in the source locale. */
|
|
565
567
|
readonly undefinedUsages: () => readonly Usage[];
|
|
566
568
|
readonly placeholders: (value: string) => readonly Placeholder[];
|
|
@@ -597,6 +599,20 @@ interface ReportInput {
|
|
|
597
599
|
readonly diagnostics: readonly Diagnostic[];
|
|
598
600
|
readonly coverage: readonly LocaleCoverage[];
|
|
599
601
|
readonly summary: ReportSummary;
|
|
602
|
+
/** Set with `x18n check --fix`; `diagnostics` are then the ones left after the fixes (or before them, in a dry run). */
|
|
603
|
+
readonly fix?: FixSummary | undefined;
|
|
604
|
+
}
|
|
605
|
+
/** What `x18n check --fix` did in a project. */
|
|
606
|
+
interface FixSummary {
|
|
607
|
+
/** False in a dry run. */
|
|
608
|
+
readonly applied: boolean;
|
|
609
|
+
/** Diagnostics whose fix is in the plan. */
|
|
610
|
+
readonly fixed: number;
|
|
611
|
+
/** Diagnostics left out because their only bulk fix is `unsafe` (pass `--fix=unsafe`). */
|
|
612
|
+
readonly skipped: number;
|
|
613
|
+
/** Files the plan writes, relative to the project root. */
|
|
614
|
+
readonly files: readonly string[];
|
|
615
|
+
readonly warnings: readonly string[];
|
|
600
616
|
}
|
|
601
617
|
/** What a check baseline left out (`x18n check --baseline`). */
|
|
602
618
|
interface BaselineSummary {
|
|
@@ -1157,11 +1173,13 @@ interface Usage {
|
|
|
1157
1173
|
readonly scopePrefix: string | undefined;
|
|
1158
1174
|
}
|
|
1159
1175
|
type Severity = "error" | "warning" | "info" | "hint";
|
|
1176
|
+
/** A `set` with `replace: false` is skipped, with a warning, when it would replace other keys or a message on its path. */
|
|
1160
1177
|
type EditOp = {
|
|
1161
1178
|
readonly type: "set";
|
|
1162
1179
|
readonly key: KeyString;
|
|
1163
1180
|
readonly locale: LocaleCode;
|
|
1164
1181
|
readonly value: MessageLeaf;
|
|
1182
|
+
readonly replace?: boolean;
|
|
1165
1183
|
} | {
|
|
1166
1184
|
readonly type: "rename";
|
|
1167
1185
|
readonly from: KeyString;
|
|
@@ -1215,9 +1233,15 @@ interface ExtractItem {
|
|
|
1215
1233
|
/** Lines to add once per file when missing, for example an import. */
|
|
1216
1234
|
readonly imports?: readonly string[];
|
|
1217
1235
|
}
|
|
1236
|
+
/**
|
|
1237
|
+
* How `x18n check --fix` may apply a fix. `safe` fixes only add, so `--fix` applies them; `unsafe` fixes can lose data, so
|
|
1238
|
+
* only `--fix=unsafe` applies them. A fix without a level is offered in the editor and never applied in bulk.
|
|
1239
|
+
*/
|
|
1240
|
+
type FixLevel = "safe" | "unsafe";
|
|
1218
1241
|
interface Fix {
|
|
1219
1242
|
readonly title: string;
|
|
1220
1243
|
readonly ops: readonly EditOp[];
|
|
1244
|
+
readonly level?: FixLevel | undefined;
|
|
1221
1245
|
}
|
|
1222
1246
|
interface Location {
|
|
1223
1247
|
readonly path: string;
|
|
@@ -1659,6 +1683,20 @@ interface UnusedDeletion {
|
|
|
1659
1683
|
/** Sorted by key. */
|
|
1660
1684
|
readonly kept: readonly KeptUnusedKey[];
|
|
1661
1685
|
}
|
|
1686
|
+
/** Which fixes `planFixes` applies. */
|
|
1687
|
+
interface FixQuery {
|
|
1688
|
+
/** Also apply `unsafe` fixes (`x18n check --fix=unsafe`). */
|
|
1689
|
+
readonly unsafe?: boolean | undefined;
|
|
1690
|
+
}
|
|
1691
|
+
/** The fixes of many diagnostics, as one plan: review it (dry run), then pass `plan` to `apply`. */
|
|
1692
|
+
interface FixPlan {
|
|
1693
|
+
/** Deletes first, then the other ops, each planned once. */
|
|
1694
|
+
readonly plan: Plan;
|
|
1695
|
+
/** The diagnostics whose fix is in the plan. */
|
|
1696
|
+
readonly fixed: readonly Diagnostic[];
|
|
1697
|
+
/** Diagnostics left out because their only bulk fix is `unsafe` and the query does not allow it. */
|
|
1698
|
+
readonly skipped: readonly Diagnostic[];
|
|
1699
|
+
}
|
|
1662
1700
|
interface ProjectExplanation {
|
|
1663
1701
|
readonly name: string;
|
|
1664
1702
|
readonly root: string;
|
|
@@ -1804,6 +1842,8 @@ interface Project {
|
|
|
1804
1842
|
readonly plan: (ops: readonly EditOp[]) => Plan;
|
|
1805
1843
|
/** Plans the deletion of the unused keys (`analyze().unused`), minus `ignoreKeys` and keys a scoped fully dynamic usage may reach. */
|
|
1806
1844
|
readonly planUnused: (query?: UnusedQuery) => UnusedDeletion;
|
|
1845
|
+
/** Plans the first fix of each diagnostic that has a `safe` fix, or also an `unsafe` one with `unsafe: true` (`x18n check --fix`). */
|
|
1846
|
+
readonly planFixes: (diagnostics: readonly Diagnostic[], query?: FixQuery) => FixPlan;
|
|
1807
1847
|
readonly apply: (plan: Plan, options?: {
|
|
1808
1848
|
readonly answers?: Answers | undefined;
|
|
1809
1849
|
}) => Promise<ApplyResult>;
|
|
@@ -1940,10 +1980,14 @@ interface LineIndex {
|
|
|
1940
1980
|
readonly line: (line: number) => string;
|
|
1941
1981
|
readonly lineCount: number;
|
|
1942
1982
|
}
|
|
1943
|
-
/**
|
|
1983
|
+
/**
|
|
1984
|
+
* Line index of a text, cached for the few texts used at a time (reporters, LSP ranges).
|
|
1985
|
+
* The least recently used text goes first, so a large resource hit by many diagnostics stays
|
|
1986
|
+
* cached while ranges in many small source files pass through.
|
|
1987
|
+
*/
|
|
1944
1988
|
export declare function createLineIndex(text: string): LineIndex;
|
|
1945
1989
|
export declare function rangeContains(range: TextRange, offset: number): boolean;
|
|
1946
1990
|
/** Fast non-cryptographic string hash (cyrb53), as hex. */
|
|
1947
1991
|
export declare function hashText(text: string): string;
|
|
1948
1992
|
//#endregion
|
|
1949
|
-
export type { Analysis, Answers, ApplyResult, ArgSource, BaselineSummary, Builtins, CacheStore, CallSpec, CapabilityConflict, Check, CheckContext, CheckIds, CheckLevel, CheckSetting, ConfigContext, ConfigDraft, ConfigOrigin, CreateProjectOptions, Decision, DecisionOption, DetectContext, Diagnostic, DiagnosticInput, Disposable, EditOp, Envelope, ErrorCode, ErrorInfo, ExtractItem, ExtractOptions, ExtractTemplate, Extractor, FileChange, FileEvent, FileScan, FileWrite, FinderContext, Fix, Format, FormatContext, FormatEntry, GlobOptions, GlossaryEntry, HardString, HistoryConfigs, HistoryContext, HistoryOptions, HistoryProvider, HistorySettings, Host, JsonObject, JsonPrimitive, JsonValue, KeptUnusedKey, KeyContext, KeyFile, KeyId, KeyInfo, KeyPattern, KeyQuery, KeySeparator, KeyString, KeyTarget, LazyFormat, LazyScanner, LibraryOptions, LineIndex, LinePosition, LocaleCode, LocaleCoverage, LocaleFinder, LocaleInfo, Location, Logger, ManifestReader, MemoryHost, Message, MessageChange, MessageHistory, MessageLeaf, MessageOp, MessageState, MessageTree, NamespaceFromPath, NamespaceOptions, NodeRange, OverlayHost, ParamsSource, ParseResult, Placeholder, PlaceholderComparer, PlaceholderProblem, PlaceholderSyntax, Plan, Plugin, PluginConfigs, PluginFile, PluginLoader, PluginOption, PluginRegistry, PluginSpecifier, PluralForm, Preset, PresetDetection, PresetEntry, PresetExplanation, PresetFactory, PresetLoader, PresetOption, Project, ProjectChange, ProjectExplanation, PromptContext, PromptData, PromptDraft, PromptIssue, PromptItem, PromptOptions, RegexScannerSpec, RelatedValue, ReportInput, ReportSummary, Reporter, ReporterContext, ReporterLoader, ResolveConfigOptions, ResolvedConfig, Resource, ResourceBlock, ResourceCandidate, ResourceId, ResourceNode, ResourceSpec, ResourceTransform, ResourceTransformContext, ReviewAuthor, ReviewComment, ReviewData, ReviewEdit, ReviewEditContext, ReviewFile, ReviewFilter, ReviewOptions, ReviewState, ReviewStore, ReviewThread, ReviewType, ScanContext, ScanResult, Scanner, ScannerEntry, SchemaIssue, Scope, SearchMatch, SearchOptions, SerializableConfig, Setup, Severity, SourceFile, TextEdit, TextRange, ThreadComment, TreeLeaf, UnusedDeletion, UnusedKeyDeletion, UnusedQuery, Usage, UsageCandidate, UsageContext, UsageKind, UsageMatch, UsageOptions, UsageQuery, UserConfig, WireDiagnostic, WireRange, WriteOptions, X18nError };
|
|
1993
|
+
export type { Analysis, Answers, ApplyResult, ArgSource, BaselineSummary, Builtins, CacheStore, CallSpec, CapabilityConflict, Check, CheckContext, CheckIds, CheckLevel, CheckSetting, ConfigContext, ConfigDraft, ConfigOrigin, CreateProjectOptions, Decision, DecisionOption, DetectContext, Diagnostic, DiagnosticInput, Disposable, EditOp, Envelope, ErrorCode, ErrorInfo, ExtractItem, ExtractOptions, ExtractTemplate, Extractor, FileChange, FileEvent, FileScan, FileWrite, FinderContext, Fix, FixLevel, FixPlan, FixQuery, FixSummary, Format, FormatContext, FormatEntry, GlobOptions, GlossaryEntry, HardString, HistoryConfigs, HistoryContext, HistoryOptions, HistoryProvider, HistorySettings, Host, JsonObject, JsonPrimitive, JsonValue, KeptUnusedKey, KeyContext, KeyFile, KeyId, KeyInfo, KeyPattern, KeyQuery, KeySeparator, KeyString, KeyTarget, LazyFormat, LazyScanner, LibraryOptions, LineIndex, LinePosition, LocaleCode, LocaleCoverage, LocaleFinder, LocaleInfo, Location, Logger, ManifestReader, MemoryHost, Message, MessageChange, MessageHistory, MessageLeaf, MessageOp, MessageState, MessageTree, NamespaceFromPath, NamespaceOptions, NodeRange, OverlayHost, ParamsSource, ParseResult, Placeholder, PlaceholderComparer, PlaceholderProblem, PlaceholderSyntax, Plan, Plugin, PluginConfigs, PluginFile, PluginLoader, PluginOption, PluginRegistry, PluginSpecifier, PluralForm, Preset, PresetDetection, PresetEntry, PresetExplanation, PresetFactory, PresetLoader, PresetOption, Project, ProjectChange, ProjectExplanation, PromptContext, PromptData, PromptDraft, PromptIssue, PromptItem, PromptOptions, RegexScannerSpec, RelatedValue, ReportInput, ReportSummary, Reporter, ReporterContext, ReporterLoader, ResolveConfigOptions, ResolvedConfig, Resource, ResourceBlock, ResourceCandidate, ResourceId, ResourceNode, ResourceSpec, ResourceTransform, ResourceTransformContext, ReviewAuthor, ReviewComment, ReviewData, ReviewEdit, ReviewEditContext, ReviewFile, ReviewFilter, ReviewOptions, ReviewState, ReviewStore, ReviewThread, ReviewType, ScanContext, ScanResult, Scanner, ScannerEntry, SchemaIssue, Scope, SearchMatch, SearchOptions, SerializableConfig, Setup, Severity, SourceFile, TextEdit, TextRange, ThreadComment, TreeLeaf, UnusedDeletion, UnusedKeyDeletion, UnusedQuery, Usage, UsageCandidate, UsageContext, UsageKind, UsageMatch, UsageOptions, UsageQuery, UserConfig, WireDiagnostic, WireRange, WriteOptions, X18nError };
|
package/dist/index.mjs
CHANGED
|
@@ -29,14 +29,19 @@ function applyTextEdits(text, edits) {
|
|
|
29
29
|
return out;
|
|
30
30
|
}
|
|
31
31
|
const lineIndexes = /* @__PURE__ */ new Map();
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Line index of a text, cached for the few texts used at a time (reporters, LSP ranges).
|
|
34
|
+
* The least recently used text goes first, so a large resource hit by many diagnostics stays
|
|
35
|
+
* cached while ranges in many small source files pass through.
|
|
36
|
+
*/
|
|
33
37
|
function createLineIndex(text) {
|
|
34
38
|
let index = lineIndexes.get(text);
|
|
35
|
-
if (
|
|
39
|
+
if (index) lineIndexes.delete(text);
|
|
40
|
+
else {
|
|
36
41
|
if (lineIndexes.size >= 16) lineIndexes.delete(lineIndexes.keys().next().value);
|
|
37
42
|
index = buildLineIndex(text);
|
|
38
|
-
lineIndexes.set(text, index);
|
|
39
43
|
}
|
|
44
|
+
lineIndexes.set(text, index);
|
|
40
45
|
return index;
|
|
41
46
|
}
|
|
42
47
|
function buildLineIndex(text) {
|
|
@@ -1119,6 +1124,9 @@ function renderExplanation(e, configFile) {
|
|
|
1119
1124
|
return lines.join("\n");
|
|
1120
1125
|
}
|
|
1121
1126
|
//#endregion
|
|
1127
|
+
//#region package.json
|
|
1128
|
+
var version = "0.1.10";
|
|
1129
|
+
//#endregion
|
|
1122
1130
|
//#region src/project/history.ts
|
|
1123
1131
|
const localChange = { updatedAt: Number.POSITIVE_INFINITY };
|
|
1124
1132
|
/** A target message is stale when it last changed before its source message. */
|
|
@@ -1172,6 +1180,90 @@ function createHistory(input) {
|
|
|
1172
1180
|
};
|
|
1173
1181
|
}
|
|
1174
1182
|
//#endregion
|
|
1183
|
+
//#region src/project/unused.ts
|
|
1184
|
+
/**
|
|
1185
|
+
* True when a fully dynamic usage (`t(key)`) may reach `key`: the key lies in the usage's scope namespace and under its
|
|
1186
|
+
* scope key prefix. Undefined for a usage without a scope, which may reach any key.
|
|
1187
|
+
*/
|
|
1188
|
+
function scopeMatcher(usage, namespaced) {
|
|
1189
|
+
const namespace = namespaced ? usage.scopeNamespace : void 0;
|
|
1190
|
+
const prefix = usage.scopePrefix;
|
|
1191
|
+
if (namespace === void 0 && !prefix) return void 0;
|
|
1192
|
+
return (key) => {
|
|
1193
|
+
const id = splitKey(key, namespaced);
|
|
1194
|
+
if (namespace !== void 0 && id.namespace !== namespace) return false;
|
|
1195
|
+
if (!prefix) return true;
|
|
1196
|
+
const path = id.path.join(".");
|
|
1197
|
+
return path === prefix || path.startsWith(`${prefix}.`);
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
function dynamicReach(usages, namespaced) {
|
|
1201
|
+
const scoped = [];
|
|
1202
|
+
const unscoped = [];
|
|
1203
|
+
for (const usage of usages) {
|
|
1204
|
+
if (usage.dynamic !== "unknown") continue;
|
|
1205
|
+
const reaches = scopeMatcher(usage, namespaced);
|
|
1206
|
+
if (reaches) scoped.push({
|
|
1207
|
+
usage,
|
|
1208
|
+
reaches
|
|
1209
|
+
});
|
|
1210
|
+
else unscoped.push(usage);
|
|
1211
|
+
}
|
|
1212
|
+
return {
|
|
1213
|
+
reaching: (key) => scoped.filter((s) => s.reaches(key)).map((s) => s.usage),
|
|
1214
|
+
unscoped
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
/** The plan warning for deleting unused keys while fully dynamic usages without a scope may reach any key. */
|
|
1218
|
+
function unscopedWarning(unscoped) {
|
|
1219
|
+
const files = [...new Set(unscoped.map((usage) => usage.path))].sort();
|
|
1220
|
+
const shown = files.length > 5 ? `${files.slice(0, 5).join(", ")} and ${files.length - 5} more files` : files.join(", ");
|
|
1221
|
+
return `${unscoped.length} usage${unscoped.length === 1 ? " has" : "s have"} a fully dynamic key (in ${shown}) that may reach any key; list the keys they use in \`usage.keysInUse\` to keep them.`;
|
|
1222
|
+
}
|
|
1223
|
+
function planUnusedDeletion(options, query = {}) {
|
|
1224
|
+
const ignored = keyMatcher(options.ignoreKeys);
|
|
1225
|
+
const match = query.glob ? keyMatcher([query.glob]) : void 0;
|
|
1226
|
+
const candidates = [...options.unused].filter((key) => !ignored(key) && (match === void 0 || match(key)) && (query.namespace === void 0 || splitKey(key, options.namespaced).namespace === query.namespace)).sort();
|
|
1227
|
+
const reach = dynamicReach(options.usages, options.namespaced);
|
|
1228
|
+
const keys = [];
|
|
1229
|
+
const kept = [];
|
|
1230
|
+
for (const key of candidates) {
|
|
1231
|
+
const usages = reach.reaching(key);
|
|
1232
|
+
if (usages.length > 0) {
|
|
1233
|
+
kept.push({
|
|
1234
|
+
key,
|
|
1235
|
+
usages
|
|
1236
|
+
});
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
keys.push({
|
|
1240
|
+
key,
|
|
1241
|
+
messages: options.locations(key).filter((node) => node.leaf).map((node) => ({
|
|
1242
|
+
locale: node.locale,
|
|
1243
|
+
path: node.path
|
|
1244
|
+
}))
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
const plan = options.plan(keys.map(({ key }) => ({
|
|
1248
|
+
type: "delete",
|
|
1249
|
+
key,
|
|
1250
|
+
locales: "all"
|
|
1251
|
+
})));
|
|
1252
|
+
if (reach.unscoped.length === 0 || keys.length === 0) return {
|
|
1253
|
+
plan,
|
|
1254
|
+
keys,
|
|
1255
|
+
kept
|
|
1256
|
+
};
|
|
1257
|
+
return {
|
|
1258
|
+
plan: {
|
|
1259
|
+
...plan,
|
|
1260
|
+
warnings: [...plan.warnings, unscopedWarning(reach.unscoped)]
|
|
1261
|
+
},
|
|
1262
|
+
keys,
|
|
1263
|
+
kept
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
//#endregion
|
|
1175
1267
|
//#region src/project/usages.ts
|
|
1176
1268
|
function innermost(scopes, range) {
|
|
1177
1269
|
let best;
|
|
@@ -1510,6 +1602,7 @@ function createAnalyzer(options) {
|
|
|
1510
1602
|
return prefixes.some((prefix) => key.startsWith(prefix));
|
|
1511
1603
|
};
|
|
1512
1604
|
const unused = hasScanners ? allKeys.filter((key) => !isUsed(key)) : [];
|
|
1605
|
+
const reach = dynamicReach(state.usages, options.namespaced());
|
|
1513
1606
|
const undefinedUsages = state.usages.filter((usage) => usage.dynamic === false && usage.key !== void 0 && !isDefined(usage.key));
|
|
1514
1607
|
const placeholders = (value) => {
|
|
1515
1608
|
let found = placeholderCache.get(value);
|
|
@@ -1537,6 +1630,7 @@ function createAnalyzer(options) {
|
|
|
1537
1630
|
usages: () => state.usages,
|
|
1538
1631
|
coverage: () => coverage,
|
|
1539
1632
|
unused: () => unused,
|
|
1633
|
+
dynamicUsages: reach.reaching,
|
|
1540
1634
|
undefinedUsages: () => undefinedUsages,
|
|
1541
1635
|
placeholders,
|
|
1542
1636
|
placeholderSyntaxes: registry.placeholders,
|
|
@@ -1572,6 +1666,55 @@ function createAnalyzer(options) {
|
|
|
1572
1666
|
return { analyze };
|
|
1573
1667
|
}
|
|
1574
1668
|
//#endregion
|
|
1669
|
+
//#region src/project/fixes.ts
|
|
1670
|
+
/** The first fix of a diagnostic that the query allows: `safe`, or also `unsafe` with `unsafe: true`. */
|
|
1671
|
+
function pick(diagnostic, unsafe) {
|
|
1672
|
+
return diagnostic.fixes.find((fix) => fix.level === "safe" || unsafe && fix.level === "unsafe");
|
|
1673
|
+
}
|
|
1674
|
+
/** Identifies ops that do the same: a second `set` of a key in a locale is dropped, so the first value wins. */
|
|
1675
|
+
function opId(op) {
|
|
1676
|
+
return op.type === "set" ? JSON.stringify([
|
|
1677
|
+
op.type,
|
|
1678
|
+
op.key,
|
|
1679
|
+
op.locale
|
|
1680
|
+
]) : JSON.stringify(op);
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Plans the fixes of many diagnostics as one plan. Deletes go first, so a deleted message makes room for a key that a
|
|
1684
|
+
* later `set` with `replace: false` adds under it. Identical ops are planned once.
|
|
1685
|
+
*/
|
|
1686
|
+
function planFixes(diagnostics, options, query = {}) {
|
|
1687
|
+
const unsafe = query.unsafe === true;
|
|
1688
|
+
const fixed = [];
|
|
1689
|
+
const skipped = [];
|
|
1690
|
+
const deletes = /* @__PURE__ */ new Map();
|
|
1691
|
+
const others = /* @__PURE__ */ new Map();
|
|
1692
|
+
for (const diagnostic of diagnostics) {
|
|
1693
|
+
const fix = pick(diagnostic, unsafe);
|
|
1694
|
+
if (!fix) {
|
|
1695
|
+
if (diagnostic.fixes.some((f) => f.level === "unsafe")) skipped.push(diagnostic);
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
fixed.push(diagnostic);
|
|
1699
|
+
for (const op of fix.ops) {
|
|
1700
|
+
const ops = op.type === "delete" ? deletes : others;
|
|
1701
|
+
const id = opId(op);
|
|
1702
|
+
if (!ops.has(id)) ops.set(id, op);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
const plan = options.plan([...deletes.values(), ...others.values()]);
|
|
1706
|
+
const deletesEverywhere = [...deletes.values()].some((op) => op.type === "delete" && (op.locales === void 0 || op.locales === "all"));
|
|
1707
|
+
const { unscoped } = dynamicReach(options.usages, options.namespaced);
|
|
1708
|
+
return {
|
|
1709
|
+
plan: deletesEverywhere && unscoped.length > 0 ? {
|
|
1710
|
+
...plan,
|
|
1711
|
+
warnings: [...plan.warnings, unscopedWarning(unscoped)]
|
|
1712
|
+
} : plan,
|
|
1713
|
+
fixed,
|
|
1714
|
+
skipped
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
//#endregion
|
|
1575
1718
|
//#region src/project/reviews.ts
|
|
1576
1719
|
const noReviews = {
|
|
1577
1720
|
threads: {},
|
|
@@ -2004,10 +2147,14 @@ var PlanBuilder = class {
|
|
|
2004
2147
|
if ((before !== void 0 && !isMessageLeaf(before)) !== (after !== void 0 && !isMessageLeaf(after)) || before !== void 0 && after === void 0) entry.flat = void 0;
|
|
2005
2148
|
}
|
|
2006
2149
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2150
|
+
/**
|
|
2151
|
+
* Returns the value the set replaced in `before`. Undefined when `replace` is false and the value would replace other keys
|
|
2152
|
+
* or a message on its path: the set is then skipped with a warning.
|
|
2153
|
+
*/
|
|
2154
|
+
setValue(key, locale, value, replace = true) {
|
|
2009
2155
|
const existing = this.options.message(key, locale);
|
|
2010
2156
|
if (existing) {
|
|
2157
|
+
this.touched.add(key);
|
|
2011
2158
|
const entry = this.workFor(this.targetOf(this.writable(existing.resource)));
|
|
2012
2159
|
const before = leafAt(entry.tree, existing.storedPath);
|
|
2013
2160
|
this.pushOp(entry, {
|
|
@@ -2015,20 +2162,36 @@ var PlanBuilder = class {
|
|
|
2015
2162
|
path: existing.storedPath,
|
|
2016
2163
|
value
|
|
2017
2164
|
});
|
|
2018
|
-
return before;
|
|
2165
|
+
return { before };
|
|
2019
2166
|
}
|
|
2020
2167
|
const target = this.pickTarget(key, locale);
|
|
2021
2168
|
if (target.state) this.writable(target.state.resource.id);
|
|
2022
2169
|
const entry = this.workFor(target);
|
|
2023
2170
|
const stored = this.storedPathFor(entry, this.relativePath(key, target));
|
|
2024
2171
|
const occupied = nodeAt(entry.tree, stored);
|
|
2025
|
-
|
|
2172
|
+
const parent = occupied !== void 0 && !isMessageLeaf(occupied);
|
|
2173
|
+
if (!replace) {
|
|
2174
|
+
const leaf = stored.slice(0, -1).findIndex((_, i) => {
|
|
2175
|
+
const node = nodeAt(entry.tree, stored.slice(0, i + 1));
|
|
2176
|
+
return node !== void 0 && isMessageLeaf(node);
|
|
2177
|
+
});
|
|
2178
|
+
if (leaf !== -1) {
|
|
2179
|
+
const message = stored.slice(0, leaf + 1).join(keyJoiner(this.separatorOf(target)));
|
|
2180
|
+
this.warnings.push(`"${key}" is not added: "${message}" is a message in ${target.path}, and adding "${key}" would replace it.`);
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
if (parent) {
|
|
2184
|
+
this.warnings.push(`"${key}" is not added: it is a parent of other keys in ${target.path}, and adding it would replace them.`);
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
} else if (parent) this.warnings.push(`"${key}" is a parent of other keys in ${target.path}; they are replaced.`);
|
|
2188
|
+
this.touched.add(key);
|
|
2026
2189
|
this.pushOp(entry, {
|
|
2027
2190
|
type: "set",
|
|
2028
2191
|
path: stored,
|
|
2029
2192
|
value
|
|
2030
2193
|
});
|
|
2031
|
-
return isMessageLeaf(occupied) ? occupied : void 0;
|
|
2194
|
+
return { before: isMessageLeaf(occupied) ? occupied : void 0 };
|
|
2032
2195
|
}
|
|
2033
2196
|
deleteMessage(m) {
|
|
2034
2197
|
const s = this.writable(m.resource);
|
|
@@ -2102,7 +2265,7 @@ var PlanBuilder = class {
|
|
|
2102
2265
|
const own = this.options.resolveKey(to, m.locale);
|
|
2103
2266
|
if (!relative || own && own.path !== s.resource.path) {
|
|
2104
2267
|
this.deleteMessage(m);
|
|
2105
|
-
return this.setValue(to, m.locale, m.value);
|
|
2268
|
+
return this.setValue(to, m.locale, m.value)?.before;
|
|
2106
2269
|
}
|
|
2107
2270
|
const entry = this.workFor(this.targetOf(s));
|
|
2108
2271
|
const separator = this.separatorOf(s.resource);
|
|
@@ -2313,11 +2476,12 @@ var PlanBuilder = class {
|
|
|
2313
2476
|
for (const locale of locales) this.recordSource(op.key, locale, true);
|
|
2314
2477
|
}
|
|
2315
2478
|
set(op) {
|
|
2316
|
-
|
|
2479
|
+
const replace = op.replace !== false;
|
|
2480
|
+
if (!this.setValue(op.key, op.locale, op.value, replace)) return;
|
|
2317
2481
|
if (op.locale === this.state.sourceLocale) this.plannedSource.set(op.key, op.value);
|
|
2318
2482
|
else if (op.value !== "") this.recordSource(op.key, op.locale);
|
|
2319
2483
|
if (!this.config.write.keepFilled) return;
|
|
2320
|
-
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, "");
|
|
2484
|
+
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, "", replace);
|
|
2321
2485
|
}
|
|
2322
2486
|
run(ops) {
|
|
2323
2487
|
for (const op of ops) switch (op.type) {
|
|
@@ -2837,80 +3001,6 @@ function renderPrompt(data) {
|
|
|
2837
3001
|
return out.join("\n");
|
|
2838
3002
|
}
|
|
2839
3003
|
//#endregion
|
|
2840
|
-
//#region src/project/unused.ts
|
|
2841
|
-
/**
|
|
2842
|
-
* True when a fully dynamic usage (`t(key)`) may reach `key`: the key lies in the usage's scope namespace and under its
|
|
2843
|
-
* scope key prefix. Undefined for a usage without a scope, which may reach any key.
|
|
2844
|
-
*/
|
|
2845
|
-
function scopeMatcher(usage, namespaced) {
|
|
2846
|
-
const namespace = namespaced ? usage.scopeNamespace : void 0;
|
|
2847
|
-
const prefix = usage.scopePrefix;
|
|
2848
|
-
if (namespace === void 0 && !prefix) return void 0;
|
|
2849
|
-
return (key) => {
|
|
2850
|
-
const id = splitKey(key, namespaced);
|
|
2851
|
-
if (namespace !== void 0 && id.namespace !== namespace) return false;
|
|
2852
|
-
if (!prefix) return true;
|
|
2853
|
-
const path = id.path.join(".");
|
|
2854
|
-
return path === prefix || path.startsWith(`${prefix}.`);
|
|
2855
|
-
};
|
|
2856
|
-
}
|
|
2857
|
-
function planUnusedDeletion(options, query = {}) {
|
|
2858
|
-
const ignored = keyMatcher(options.ignoreKeys);
|
|
2859
|
-
const match = query.glob ? keyMatcher([query.glob]) : void 0;
|
|
2860
|
-
const candidates = [...options.unused].filter((key) => !ignored(key) && (match === void 0 || match(key)) && (query.namespace === void 0 || splitKey(key, options.namespaced).namespace === query.namespace)).sort();
|
|
2861
|
-
const scoped = [];
|
|
2862
|
-
const unscoped = [];
|
|
2863
|
-
for (const usage of options.usages) {
|
|
2864
|
-
if (usage.dynamic !== "unknown") continue;
|
|
2865
|
-
const reaches = scopeMatcher(usage, options.namespaced);
|
|
2866
|
-
if (reaches) scoped.push({
|
|
2867
|
-
usage,
|
|
2868
|
-
reaches
|
|
2869
|
-
});
|
|
2870
|
-
else unscoped.push(usage);
|
|
2871
|
-
}
|
|
2872
|
-
const keys = [];
|
|
2873
|
-
const kept = [];
|
|
2874
|
-
for (const key of candidates) {
|
|
2875
|
-
const usages = scoped.filter((s) => s.reaches(key)).map((s) => s.usage);
|
|
2876
|
-
if (usages.length > 0) {
|
|
2877
|
-
kept.push({
|
|
2878
|
-
key,
|
|
2879
|
-
usages
|
|
2880
|
-
});
|
|
2881
|
-
continue;
|
|
2882
|
-
}
|
|
2883
|
-
keys.push({
|
|
2884
|
-
key,
|
|
2885
|
-
messages: options.locations(key).filter((node) => node.leaf).map((node) => ({
|
|
2886
|
-
locale: node.locale,
|
|
2887
|
-
path: node.path
|
|
2888
|
-
}))
|
|
2889
|
-
});
|
|
2890
|
-
}
|
|
2891
|
-
const plan = options.plan(keys.map(({ key }) => ({
|
|
2892
|
-
type: "delete",
|
|
2893
|
-
key,
|
|
2894
|
-
locales: "all"
|
|
2895
|
-
})));
|
|
2896
|
-
if (unscoped.length === 0 || keys.length === 0) return {
|
|
2897
|
-
plan,
|
|
2898
|
-
keys,
|
|
2899
|
-
kept
|
|
2900
|
-
};
|
|
2901
|
-
const files = [...new Set(unscoped.map((usage) => usage.path))].sort();
|
|
2902
|
-
const shown = files.length > 5 ? `${files.slice(0, 5).join(", ")} and ${files.length - 5} more files` : files.join(", ");
|
|
2903
|
-
const warning = `${unscoped.length} usage${unscoped.length === 1 ? " has" : "s have"} a fully dynamic key (in ${shown}) that may reach any key; list the keys they use in \`usage.keysInUse\` to keep them.`;
|
|
2904
|
-
return {
|
|
2905
|
-
plan: {
|
|
2906
|
-
...plan,
|
|
2907
|
-
warnings: [...plan.warnings, warning]
|
|
2908
|
-
},
|
|
2909
|
-
keys,
|
|
2910
|
-
kept
|
|
2911
|
-
};
|
|
2912
|
-
}
|
|
2913
|
-
//#endregion
|
|
2914
3004
|
//#region src/project/project.ts
|
|
2915
3005
|
const ioConcurrency = 64;
|
|
2916
3006
|
async function forEachLimit(items, limit, fn) {
|
|
@@ -2950,8 +3040,10 @@ function createProject(options) {
|
|
|
2950
3040
|
const { basesOf, formOf } = derivedKeys(config.usage.derivedKeys);
|
|
2951
3041
|
const scanSignature = hashText(JSON.stringify({
|
|
2952
3042
|
usage: config.usage,
|
|
3043
|
+
extract: config.extract,
|
|
2953
3044
|
namespace: config.namespace,
|
|
2954
3045
|
scanners: registry.scanners.map((s) => s.id),
|
|
3046
|
+
version,
|
|
2955
3047
|
v: 1
|
|
2956
3048
|
}));
|
|
2957
3049
|
const localeFilter = (code) => (!config.locales || config.locales.includes(code)) && !config.ignoreLocales.includes(code);
|
|
@@ -3529,8 +3621,14 @@ function createProject(options) {
|
|
|
3529
3621
|
basesOf,
|
|
3530
3622
|
formOf,
|
|
3531
3623
|
localeInfo,
|
|
3532
|
-
text: (path) => fileTexts.get(path) ?? files.get(path)?.text
|
|
3624
|
+
text: (path) => fileTexts.get(path) ?? files.get(path)?.text,
|
|
3625
|
+
namespaced: () => namespaced
|
|
3533
3626
|
});
|
|
3627
|
+
let lastAnalysis;
|
|
3628
|
+
function analyze() {
|
|
3629
|
+
if (lastAnalysis?.revision !== revision) lastAnalysis = analyzer.analyze();
|
|
3630
|
+
return lastAnalysis;
|
|
3631
|
+
}
|
|
3534
3632
|
function keys(query = {}) {
|
|
3535
3633
|
const { messages, keyPaths } = idx();
|
|
3536
3634
|
const match = query.glob ? keyMatcher([query.glob], config.keySeparator) : void 0;
|
|
@@ -3545,7 +3643,7 @@ function createProject(options) {
|
|
|
3545
3643
|
});
|
|
3546
3644
|
}
|
|
3547
3645
|
if (query.unused) {
|
|
3548
|
-
const unused = new Set(
|
|
3646
|
+
const unused = new Set(analyze().unused);
|
|
3549
3647
|
list = list.filter((key) => unused.has(key));
|
|
3550
3648
|
}
|
|
3551
3649
|
return list.sort();
|
|
@@ -3573,6 +3671,24 @@ function createProject(options) {
|
|
|
3573
3671
|
}
|
|
3574
3672
|
return found.sort((a, b) => b.score - a.score || a.value.length - b.value.length || a.key.localeCompare(b.key));
|
|
3575
3673
|
}
|
|
3674
|
+
let diagnosticIndex;
|
|
3675
|
+
function diagnosticsOf(key) {
|
|
3676
|
+
const analysis = analyze();
|
|
3677
|
+
if (diagnosticIndex?.analysis !== analysis) {
|
|
3678
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
3679
|
+
for (const d of analysis.diagnostics) {
|
|
3680
|
+
if (d.key === void 0) continue;
|
|
3681
|
+
const list = byKey.get(d.key);
|
|
3682
|
+
if (list) list.push(d);
|
|
3683
|
+
else byKey.set(d.key, [d]);
|
|
3684
|
+
}
|
|
3685
|
+
diagnosticIndex = {
|
|
3686
|
+
analysis,
|
|
3687
|
+
byKey
|
|
3688
|
+
};
|
|
3689
|
+
}
|
|
3690
|
+
return [...diagnosticIndex.byKey.get(key) ?? []];
|
|
3691
|
+
}
|
|
3576
3692
|
function get(key) {
|
|
3577
3693
|
const { messages, keyPaths } = idx();
|
|
3578
3694
|
const byLocale = messages.get(key);
|
|
@@ -3587,7 +3703,7 @@ function createProject(options) {
|
|
|
3587
3703
|
messages: list,
|
|
3588
3704
|
missing: locales.filter((locale) => !byLocale.has(locale) && needs(key, locale)),
|
|
3589
3705
|
usages: usageMatches({ key }),
|
|
3590
|
-
diagnostics:
|
|
3706
|
+
diagnostics: diagnosticsOf(key),
|
|
3591
3707
|
reviews: reviews({ key })
|
|
3592
3708
|
};
|
|
3593
3709
|
}
|
|
@@ -3809,7 +3925,7 @@ function createProject(options) {
|
|
|
3809
3925
|
...h,
|
|
3810
3926
|
path: p
|
|
3811
3927
|
}))),
|
|
3812
|
-
analyze
|
|
3928
|
+
analyze,
|
|
3813
3929
|
async loadHistory(historyOptions = {}) {
|
|
3814
3930
|
await ready;
|
|
3815
3931
|
await history.load(historyOptions);
|
|
@@ -3832,13 +3948,18 @@ function createProject(options) {
|
|
|
3832
3948
|
owns: (path) => fileTexts.has(path) || files.has(path) || usageEligible(path),
|
|
3833
3949
|
plan: (ops) => planner.plan(ops),
|
|
3834
3950
|
planUnused: (query) => planUnusedDeletion({
|
|
3835
|
-
unused:
|
|
3951
|
+
unused: analyze().unused,
|
|
3836
3952
|
usages: idx().usages,
|
|
3837
3953
|
namespaced,
|
|
3838
3954
|
ignoreKeys: config.ignoreKeys,
|
|
3839
3955
|
locations: (key) => project.locations(key),
|
|
3840
3956
|
plan: (ops) => planner.plan(ops)
|
|
3841
3957
|
}, query),
|
|
3958
|
+
planFixes: (diagnostics, query) => planFixes(diagnostics, {
|
|
3959
|
+
usages: idx().usages,
|
|
3960
|
+
namespaced,
|
|
3961
|
+
plan: (ops) => planner.plan(ops)
|
|
3962
|
+
}, query),
|
|
3842
3963
|
apply: (plan, applyOptions) => planner.apply(plan, applyOptions?.answers ?? {}),
|
|
3843
3964
|
update,
|
|
3844
3965
|
watch() {
|
package/package.json
CHANGED