@x18n/core 0.1.8 → 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 +83 -7
- package/dist/index.mjs +348 -133
- 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 {
|
|
@@ -650,6 +666,29 @@ interface ResourceTransform {
|
|
|
650
666
|
/** Descriptions that the stored tree holds beside the messages, such as `{ "name": { "message": "...", "description": "..." } }`, keyed by `pathId(storedPath)`. */
|
|
651
667
|
readonly describe?: ((tree: MessageTree, ctx: ResourceTransformContext) => ReadonlyMap<string, string>) | undefined;
|
|
652
668
|
}
|
|
669
|
+
/** A review edit of a plan, after it is applied to the review data. */
|
|
670
|
+
interface ReviewEdit {
|
|
671
|
+
/** The review type of a new comment, or the op that resolved or applied one. */
|
|
672
|
+
readonly type: ReviewType | "resolve-review" | "apply-suggestion";
|
|
673
|
+
readonly key: KeyString;
|
|
674
|
+
readonly locale: LocaleCode;
|
|
675
|
+
/** The new comment, or the comment that was resolved or applied. */
|
|
676
|
+
readonly comment: ReviewComment;
|
|
677
|
+
/** The thread of the message after the edit, oldest first. */
|
|
678
|
+
readonly comments: readonly ReviewComment[];
|
|
679
|
+
}
|
|
680
|
+
interface ReviewEditContext {
|
|
681
|
+
/** A message before the plan. */
|
|
682
|
+
readonly message: (key: KeyString, locale: LocaleCode) => Message | undefined;
|
|
683
|
+
readonly resource: (id: ResourceId) => Resource | undefined;
|
|
684
|
+
/** The text of a project file as the plan leaves it so far, including earlier writes of `reviewEdit` hooks. */
|
|
685
|
+
readonly text: (path: string) => string | undefined;
|
|
686
|
+
}
|
|
687
|
+
/** The full new content of a file, which core puts into the plan. */
|
|
688
|
+
interface FileWrite {
|
|
689
|
+
readonly path: string;
|
|
690
|
+
readonly content: string;
|
|
691
|
+
}
|
|
653
692
|
interface PromptDraft {
|
|
654
693
|
readonly rules: string[];
|
|
655
694
|
readonly notes: string[];
|
|
@@ -688,6 +727,8 @@ interface Plugin {
|
|
|
688
727
|
readonly derivedKeys?: readonly string[] | undefined;
|
|
689
728
|
readonly transformResource?: ResourceTransform | undefined;
|
|
690
729
|
readonly translatePrompt?: ((draft: PromptDraft, ctx: PromptContext) => void) | undefined;
|
|
730
|
+
/** Called for each review edit of a plan, in op order. The writes it returns go into the same plan. */
|
|
731
|
+
readonly reviewEdit?: ((edit: ReviewEdit, ctx: ReviewEditContext) => readonly FileWrite[] | undefined) | undefined;
|
|
691
732
|
/** Display data for a locale code. The first plugin that returns a value wins. */
|
|
692
733
|
readonly localeInfo?: ((code: LocaleCode) => Partial<LocaleInfo> | undefined) | undefined;
|
|
693
734
|
}
|
|
@@ -917,7 +958,7 @@ interface SerializableConfig {
|
|
|
917
958
|
} | undefined;
|
|
918
959
|
/** Turn every warning into an error. */
|
|
919
960
|
readonly strict?: boolean | undefined;
|
|
920
|
-
/** Keys (globs) excluded from all checks. */
|
|
961
|
+
/** Keys (globs) excluded from all checks. `x18n.config.ts` also takes regular expressions. */
|
|
921
962
|
readonly ignoreKeys?: readonly string[] | undefined;
|
|
922
963
|
readonly glossary?: readonly GlossaryEntry[] | undefined;
|
|
923
964
|
/** History providers for stale translations. `false` turns every provider off. Options of a provider sit under its key, such as `git: { maxDepth, since }` (or `git: false`). */
|
|
@@ -940,9 +981,13 @@ interface HistorySettings extends HistoryOptions {
|
|
|
940
981
|
}
|
|
941
982
|
type PresetOption = Preset | PluginSpecifier | false | undefined;
|
|
942
983
|
type PluginOption = Plugin | Preset | PluginSpecifier | false | undefined;
|
|
984
|
+
/** A key glob (`*` inside a segment, `**` across segments), or a regular expression tested against the whole key. */
|
|
985
|
+
type KeyPattern = string | RegExp;
|
|
943
986
|
/** The config accepted by `defineConfig`: the serializable part plus plugin objects. */
|
|
944
|
-
interface UserConfig extends Omit<SerializableConfig, "presets" | "plugins" | "checks" | "history"> {
|
|
987
|
+
interface UserConfig extends Omit<SerializableConfig, "presets" | "plugins" | "checks" | "history" | "ignoreKeys"> {
|
|
945
988
|
readonly presets?: "auto" | readonly PresetOption[] | undefined;
|
|
989
|
+
/** Keys excluded from all checks: globs, or regular expressions tested against the whole key (`ns:a.b` when namespaced). */
|
|
990
|
+
readonly ignoreKeys?: readonly KeyPattern[] | undefined;
|
|
946
991
|
/** History providers for stale translations: `false` turns them off; provider options such as `git` sit under their key; `providers` adds providers asked first. */
|
|
947
992
|
readonly history?: boolean | (HistoryOptions & {
|
|
948
993
|
readonly providers?: readonly HistoryProvider[] | undefined;
|
|
@@ -998,7 +1043,7 @@ interface ResolvedConfig {
|
|
|
998
1043
|
readonly [checkId: string]: CheckSetting;
|
|
999
1044
|
};
|
|
1000
1045
|
readonly strict: boolean;
|
|
1001
|
-
readonly ignoreKeys: readonly
|
|
1046
|
+
readonly ignoreKeys: readonly KeyPattern[];
|
|
1002
1047
|
readonly glossary: readonly GlossaryEntry[];
|
|
1003
1048
|
readonly history: false | HistorySettings;
|
|
1004
1049
|
readonly review: {
|
|
@@ -1128,11 +1173,13 @@ interface Usage {
|
|
|
1128
1173
|
readonly scopePrefix: string | undefined;
|
|
1129
1174
|
}
|
|
1130
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. */
|
|
1131
1177
|
type EditOp = {
|
|
1132
1178
|
readonly type: "set";
|
|
1133
1179
|
readonly key: KeyString;
|
|
1134
1180
|
readonly locale: LocaleCode;
|
|
1135
1181
|
readonly value: MessageLeaf;
|
|
1182
|
+
readonly replace?: boolean;
|
|
1136
1183
|
} | {
|
|
1137
1184
|
readonly type: "rename";
|
|
1138
1185
|
readonly from: KeyString;
|
|
@@ -1186,9 +1233,15 @@ interface ExtractItem {
|
|
|
1186
1233
|
/** Lines to add once per file when missing, for example an import. */
|
|
1187
1234
|
readonly imports?: readonly string[];
|
|
1188
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";
|
|
1189
1241
|
interface Fix {
|
|
1190
1242
|
readonly title: string;
|
|
1191
1243
|
readonly ops: readonly EditOp[];
|
|
1244
|
+
readonly level?: FixLevel | undefined;
|
|
1192
1245
|
}
|
|
1193
1246
|
interface Location {
|
|
1194
1247
|
readonly path: string;
|
|
@@ -1283,8 +1336,11 @@ export declare function replaceKeyPrefix(key: KeyString, from: KeyString, to: Ke
|
|
|
1283
1336
|
* `{a,b}` alternates, `[...]` is a character class.
|
|
1284
1337
|
*/
|
|
1285
1338
|
export declare function globToRegExp(glob: string, separator?: string): RegExp;
|
|
1286
|
-
/**
|
|
1287
|
-
|
|
1339
|
+
/**
|
|
1340
|
+
* Matcher over key patterns. In a glob `*` stays inside a segment and `**` spans segments; with literal keys
|
|
1341
|
+
* (`separator: false`) `*` spans the whole key. A regular expression is tested against the whole key as is.
|
|
1342
|
+
*/
|
|
1343
|
+
export declare function keyMatcher(patterns: readonly KeyPattern[], separator?: KeySeparator): (key: KeyString) => boolean;
|
|
1288
1344
|
/** Matcher over posix file paths. */
|
|
1289
1345
|
export declare function pathMatcher(globs: readonly string[]): (path: string) => boolean;
|
|
1290
1346
|
//#endregion
|
|
@@ -1627,6 +1683,20 @@ interface UnusedDeletion {
|
|
|
1627
1683
|
/** Sorted by key. */
|
|
1628
1684
|
readonly kept: readonly KeptUnusedKey[];
|
|
1629
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
|
+
}
|
|
1630
1700
|
interface ProjectExplanation {
|
|
1631
1701
|
readonly name: string;
|
|
1632
1702
|
readonly root: string;
|
|
@@ -1772,6 +1842,8 @@ interface Project {
|
|
|
1772
1842
|
readonly plan: (ops: readonly EditOp[]) => Plan;
|
|
1773
1843
|
/** Plans the deletion of the unused keys (`analyze().unused`), minus `ignoreKeys` and keys a scoped fully dynamic usage may reach. */
|
|
1774
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;
|
|
1775
1847
|
readonly apply: (plan: Plan, options?: {
|
|
1776
1848
|
readonly answers?: Answers | undefined;
|
|
1777
1849
|
}) => Promise<ApplyResult>;
|
|
@@ -1908,10 +1980,14 @@ interface LineIndex {
|
|
|
1908
1980
|
readonly line: (line: number) => string;
|
|
1909
1981
|
readonly lineCount: number;
|
|
1910
1982
|
}
|
|
1911
|
-
/**
|
|
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
|
+
*/
|
|
1912
1988
|
export declare function createLineIndex(text: string): LineIndex;
|
|
1913
1989
|
export declare function rangeContains(range: TextRange, offset: number): boolean;
|
|
1914
1990
|
/** Fast non-cryptographic string hash (cyrb53), as hex. */
|
|
1915
1991
|
export declare function hashText(text: string): string;
|
|
1916
1992
|
//#endregion
|
|
1917
|
-
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, FinderContext, Fix, Format, FormatContext, FormatEntry, GlobOptions, GlossaryEntry, HardString, HistoryConfigs, HistoryContext, HistoryOptions, HistoryProvider, HistorySettings, Host, JsonObject, JsonPrimitive, JsonValue, KeptUnusedKey, KeyContext, KeyFile, KeyId, KeyInfo, 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, 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) {
|
|
@@ -379,10 +384,13 @@ function globToRegExp(glob, separator = "/") {
|
|
|
379
384
|
}
|
|
380
385
|
return new RegExp(`^${out}$`);
|
|
381
386
|
}
|
|
382
|
-
/**
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
387
|
+
/**
|
|
388
|
+
* Matcher over key patterns. In a glob `*` stays inside a segment and `**` spans segments; with literal keys
|
|
389
|
+
* (`separator: false`) `*` spans the whole key. A regular expression is tested against the whole key as is.
|
|
390
|
+
*/
|
|
391
|
+
function keyMatcher(patterns, separator = ".") {
|
|
392
|
+
if (patterns.length === 0) return () => false;
|
|
393
|
+
const regexes = patterns.map((pattern) => pattern instanceof RegExp ? new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, "")) : globToRegExp(pattern, separator === false ? "\0" : separator));
|
|
386
394
|
return (key) => regexes.some((re) => re.test(key));
|
|
387
395
|
}
|
|
388
396
|
/** Matcher over posix file paths. */
|
|
@@ -621,11 +629,15 @@ function deleteIn(tree, path) {
|
|
|
621
629
|
delete parents[i - 1][path[i - 1]];
|
|
622
630
|
}
|
|
623
631
|
}
|
|
632
|
+
/** Applies ops to the tree itself. Parent objects left empty are removed. */
|
|
633
|
+
function applyMessageOpsInPlace(tree, ops) {
|
|
634
|
+
for (const op of ops) if (op.type === "set") setIn(tree, op.path, op.value);
|
|
635
|
+
else deleteIn(tree, op.path);
|
|
636
|
+
}
|
|
624
637
|
/** Applies ops to a copy of the tree. Parent objects left empty are removed. */
|
|
625
638
|
function applyMessageOps(tree, ops) {
|
|
626
639
|
const out = cloneTree(tree);
|
|
627
|
-
|
|
628
|
-
else deleteIn(out, op.path);
|
|
640
|
+
applyMessageOpsInPlace(out, ops);
|
|
629
641
|
return out;
|
|
630
642
|
}
|
|
631
643
|
/** Sorts keys at every level. */
|
|
@@ -1112,6 +1124,9 @@ function renderExplanation(e, configFile) {
|
|
|
1112
1124
|
return lines.join("\n");
|
|
1113
1125
|
}
|
|
1114
1126
|
//#endregion
|
|
1127
|
+
//#region package.json
|
|
1128
|
+
var version = "0.1.10";
|
|
1129
|
+
//#endregion
|
|
1115
1130
|
//#region src/project/history.ts
|
|
1116
1131
|
const localChange = { updatedAt: Number.POSITIVE_INFINITY };
|
|
1117
1132
|
/** A target message is stale when it last changed before its source message. */
|
|
@@ -1165,6 +1180,90 @@ function createHistory(input) {
|
|
|
1165
1180
|
};
|
|
1166
1181
|
}
|
|
1167
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
|
|
1168
1267
|
//#region src/project/usages.ts
|
|
1169
1268
|
function innermost(scopes, range) {
|
|
1170
1269
|
let best;
|
|
@@ -1503,6 +1602,7 @@ function createAnalyzer(options) {
|
|
|
1503
1602
|
return prefixes.some((prefix) => key.startsWith(prefix));
|
|
1504
1603
|
};
|
|
1505
1604
|
const unused = hasScanners ? allKeys.filter((key) => !isUsed(key)) : [];
|
|
1605
|
+
const reach = dynamicReach(state.usages, options.namespaced());
|
|
1506
1606
|
const undefinedUsages = state.usages.filter((usage) => usage.dynamic === false && usage.key !== void 0 && !isDefined(usage.key));
|
|
1507
1607
|
const placeholders = (value) => {
|
|
1508
1608
|
let found = placeholderCache.get(value);
|
|
@@ -1530,6 +1630,7 @@ function createAnalyzer(options) {
|
|
|
1530
1630
|
usages: () => state.usages,
|
|
1531
1631
|
coverage: () => coverage,
|
|
1532
1632
|
unused: () => unused,
|
|
1633
|
+
dynamicUsages: reach.reaching,
|
|
1533
1634
|
undefinedUsages: () => undefinedUsages,
|
|
1534
1635
|
placeholders,
|
|
1535
1636
|
placeholderSyntaxes: registry.placeholders,
|
|
@@ -1565,6 +1666,55 @@ function createAnalyzer(options) {
|
|
|
1565
1666
|
return { analyze };
|
|
1566
1667
|
}
|
|
1567
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
|
|
1568
1718
|
//#region src/project/reviews.ts
|
|
1569
1719
|
const noReviews = {
|
|
1570
1720
|
threads: {},
|
|
@@ -1736,6 +1886,10 @@ function nodeAt(tree, path) {
|
|
|
1736
1886
|
}
|
|
1737
1887
|
return node;
|
|
1738
1888
|
}
|
|
1889
|
+
function leafAt(tree, path) {
|
|
1890
|
+
const node = nodeAt(tree, path);
|
|
1891
|
+
return isMessageLeaf(node) ? node : void 0;
|
|
1892
|
+
}
|
|
1739
1893
|
/** True for flat files (`{ "a.b": "x" }`), false for nested ones, undefined when it cannot tell. */
|
|
1740
1894
|
function isFlatTree(tree, separator) {
|
|
1741
1895
|
const entries = Object.entries(tree);
|
|
@@ -1795,6 +1949,7 @@ var PlanBuilder = class {
|
|
|
1795
1949
|
config;
|
|
1796
1950
|
/** Source values set by this plan, for the source records of targets set after them. */
|
|
1797
1951
|
plannedSource = /* @__PURE__ */ new Map();
|
|
1952
|
+
reviewEdits = [];
|
|
1798
1953
|
draft;
|
|
1799
1954
|
constructor(options, state, answers) {
|
|
1800
1955
|
this.options = options;
|
|
@@ -1822,7 +1977,7 @@ var PlanBuilder = class {
|
|
|
1822
1977
|
if (!entry) {
|
|
1823
1978
|
entry = {
|
|
1824
1979
|
target,
|
|
1825
|
-
tree: target.state?.rawTree ?? {},
|
|
1980
|
+
tree: cloneTree(target.state?.rawTree ?? {}),
|
|
1826
1981
|
ops: [],
|
|
1827
1982
|
keyRenames: [],
|
|
1828
1983
|
formatOnly: false
|
|
@@ -1866,7 +2021,8 @@ var PlanBuilder = class {
|
|
|
1866
2021
|
const stored = this.options.toStored(entry.target, relative);
|
|
1867
2022
|
const separator = this.separatorOf(entry.target);
|
|
1868
2023
|
if (separator === false) return stored;
|
|
1869
|
-
|
|
2024
|
+
if (stored.length === relative.length) entry.flat ??= isFlatTree(entry.tree, separator);
|
|
2025
|
+
return stored.length === relative.length && (entry.flat ?? this.config.keyStyle === "flat") ? [stored.join(separator)] : stored;
|
|
1870
2026
|
}
|
|
1871
2027
|
createTarget(key, locale) {
|
|
1872
2028
|
const { resources, sourceLocale, namespaced } = this.state;
|
|
@@ -1979,30 +2135,63 @@ var PlanBuilder = class {
|
|
|
1979
2135
|
/** ---- Message ops ---- */
|
|
1980
2136
|
pushOp(entry, op) {
|
|
1981
2137
|
entry.ops.push(op);
|
|
1982
|
-
|
|
2138
|
+
this.applyOps(entry, [op]);
|
|
2139
|
+
}
|
|
2140
|
+
/** Applies ops to the entry's tree in place; drops `flat` when a top-level object appears or goes, or a top-level key goes. */
|
|
2141
|
+
applyOps(entry, ops) {
|
|
2142
|
+
for (const op of ops) {
|
|
2143
|
+
const top = op.path[0];
|
|
2144
|
+
const before = entry.tree[top];
|
|
2145
|
+
applyMessageOpsInPlace(entry.tree, [op]);
|
|
2146
|
+
const after = entry.tree[top];
|
|
2147
|
+
if ((before !== void 0 && !isMessageLeaf(before)) !== (after !== void 0 && !isMessageLeaf(after)) || before !== void 0 && after === void 0) entry.flat = void 0;
|
|
2148
|
+
}
|
|
1983
2149
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
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) {
|
|
1986
2155
|
const existing = this.options.message(key, locale);
|
|
1987
2156
|
if (existing) {
|
|
1988
|
-
this.
|
|
2157
|
+
this.touched.add(key);
|
|
2158
|
+
const entry = this.workFor(this.targetOf(this.writable(existing.resource)));
|
|
2159
|
+
const before = leafAt(entry.tree, existing.storedPath);
|
|
2160
|
+
this.pushOp(entry, {
|
|
1989
2161
|
type: "set",
|
|
1990
2162
|
path: existing.storedPath,
|
|
1991
2163
|
value
|
|
1992
2164
|
});
|
|
1993
|
-
return;
|
|
2165
|
+
return { before };
|
|
1994
2166
|
}
|
|
1995
2167
|
const target = this.pickTarget(key, locale);
|
|
1996
2168
|
if (target.state) this.writable(target.state.resource.id);
|
|
1997
2169
|
const entry = this.workFor(target);
|
|
1998
2170
|
const stored = this.storedPathFor(entry, this.relativePath(key, target));
|
|
1999
2171
|
const occupied = nodeAt(entry.tree, stored);
|
|
2000
|
-
|
|
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);
|
|
2001
2189
|
this.pushOp(entry, {
|
|
2002
2190
|
type: "set",
|
|
2003
2191
|
path: stored,
|
|
2004
2192
|
value
|
|
2005
2193
|
});
|
|
2194
|
+
return { before: isMessageLeaf(occupied) ? occupied : void 0 };
|
|
2006
2195
|
}
|
|
2007
2196
|
deleteMessage(m) {
|
|
2008
2197
|
const s = this.writable(m.resource);
|
|
@@ -2046,7 +2235,7 @@ var PlanBuilder = class {
|
|
|
2046
2235
|
text: last
|
|
2047
2236
|
});
|
|
2048
2237
|
const leaves = [...walkLeaves(node, stored)];
|
|
2049
|
-
|
|
2238
|
+
this.applyOps(entry, [...leaves.map((leaf) => ({
|
|
2050
2239
|
type: "delete",
|
|
2051
2240
|
path: leaf.path
|
|
2052
2241
|
})), ...leaves.map((leaf) => ({
|
|
@@ -2061,45 +2250,54 @@ var PlanBuilder = class {
|
|
|
2061
2250
|
moveKey(from, to, renamed = /* @__PURE__ */ new Set()) {
|
|
2062
2251
|
this.touched.add(from);
|
|
2063
2252
|
this.touched.add(to);
|
|
2064
|
-
for (const
|
|
2065
|
-
const m = list[0];
|
|
2253
|
+
for (const list of this.options.messages(from)?.values() ?? []) for (const m of [...list].reverse()) {
|
|
2066
2254
|
if (renamed.has(m.resource)) continue;
|
|
2067
|
-
const
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
this.deleteMessage(m);
|
|
2072
|
-
this.setValue(to, locale, m.value);
|
|
2073
|
-
continue;
|
|
2074
|
-
}
|
|
2075
|
-
const entry = this.workFor(this.targetOf(s));
|
|
2076
|
-
const separator = this.separatorOf(s.resource);
|
|
2077
|
-
const flat = separator !== false && m.storedPath.length === 1 && m.storedPath[0].includes(separator);
|
|
2078
|
-
const next = flat ? [relative.join(separator)] : this.options.toStored(s.resource, relative);
|
|
2079
|
-
const sameParent = next.length === m.storedPath.length && next.slice(0, -1).every((segment, i) => segment === m.storedPath[i]);
|
|
2080
|
-
const last = next.at(-1);
|
|
2081
|
-
if (sameParent && m.range && (flat ? safeFlat : safeSegment).test(last)) {
|
|
2082
|
-
entry.keyRenames.push({
|
|
2083
|
-
range: m.range.key,
|
|
2084
|
-
text: last
|
|
2085
|
-
});
|
|
2086
|
-
entry.tree = applyMessageOps(entry.tree, [{
|
|
2087
|
-
type: "delete",
|
|
2088
|
-
path: m.storedPath
|
|
2089
|
-
}, {
|
|
2090
|
-
type: "set",
|
|
2091
|
-
path: next,
|
|
2092
|
-
value: m.value
|
|
2093
|
-
}]);
|
|
2094
|
-
continue;
|
|
2255
|
+
const replaced = this.moveMessage(m, to);
|
|
2256
|
+
if (replaced !== void 0 && replaced !== m.value) {
|
|
2257
|
+
const path = this.stateById.get(m.resource)?.resource.path ?? m.resource;
|
|
2258
|
+
this.warnings.push(`Copies of "${from}" in ${m.locale} have different values; "${to}" keeps the one from ${path}.`);
|
|
2095
2259
|
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
moveMessage(m, to) {
|
|
2263
|
+
const s = this.writable(m.resource);
|
|
2264
|
+
const relative = this.relativePath(to, s.resource);
|
|
2265
|
+
const own = this.options.resolveKey(to, m.locale);
|
|
2266
|
+
if (!relative || own && own.path !== s.resource.path) {
|
|
2096
2267
|
this.deleteMessage(m);
|
|
2097
|
-
this.
|
|
2268
|
+
return this.setValue(to, m.locale, m.value)?.before;
|
|
2269
|
+
}
|
|
2270
|
+
const entry = this.workFor(this.targetOf(s));
|
|
2271
|
+
const separator = this.separatorOf(s.resource);
|
|
2272
|
+
const flat = separator !== false && m.storedPath.length === 1 && m.storedPath[0].includes(separator);
|
|
2273
|
+
const next = flat ? [relative.join(separator)] : this.options.toStored(s.resource, relative);
|
|
2274
|
+
const sameParent = next.length === m.storedPath.length && next.slice(0, -1).every((segment, i) => segment === m.storedPath[i]);
|
|
2275
|
+
const last = next.at(-1);
|
|
2276
|
+
if (sameParent && m.range && (flat ? safeFlat : safeSegment).test(last)) {
|
|
2277
|
+
const replaced = leafAt(entry.tree, next);
|
|
2278
|
+
entry.keyRenames.push({
|
|
2279
|
+
range: m.range.key,
|
|
2280
|
+
text: last
|
|
2281
|
+
});
|
|
2282
|
+
this.applyOps(entry, [{
|
|
2283
|
+
type: "delete",
|
|
2284
|
+
path: m.storedPath
|
|
2285
|
+
}, {
|
|
2098
2286
|
type: "set",
|
|
2099
|
-
path:
|
|
2287
|
+
path: next,
|
|
2100
2288
|
value: m.value
|
|
2101
|
-
});
|
|
2289
|
+
}]);
|
|
2290
|
+
return replaced;
|
|
2102
2291
|
}
|
|
2292
|
+
this.deleteMessage(m);
|
|
2293
|
+
const path = this.storedPathFor(entry, relative);
|
|
2294
|
+
const replaced = leafAt(entry.tree, path);
|
|
2295
|
+
this.pushOp(entry, {
|
|
2296
|
+
type: "set",
|
|
2297
|
+
path,
|
|
2298
|
+
value: m.value
|
|
2299
|
+
});
|
|
2300
|
+
return replaced;
|
|
2103
2301
|
}
|
|
2104
2302
|
/** The raw text a usage needs to refer to `next`, given its scope. */
|
|
2105
2303
|
rawFor(usage, next) {
|
|
@@ -2239,6 +2437,17 @@ var PlanBuilder = class {
|
|
|
2239
2437
|
...op.review === "suggestion" && { suggestion: op.suggestion },
|
|
2240
2438
|
...op.review === "approve" && current !== void 0 && { value: valueText(current) }
|
|
2241
2439
|
});
|
|
2440
|
+
this.reviewEdited(draft, op.review, op.id);
|
|
2441
|
+
}
|
|
2442
|
+
reviewEdited(draft, type, id) {
|
|
2443
|
+
const { key, locale, comment } = findReview(draft.data, id);
|
|
2444
|
+
this.reviewEdits.push({
|
|
2445
|
+
type,
|
|
2446
|
+
key,
|
|
2447
|
+
locale,
|
|
2448
|
+
comment,
|
|
2449
|
+
comments: draft.data.threads[key][locale]
|
|
2450
|
+
});
|
|
2242
2451
|
}
|
|
2243
2452
|
applySuggestion(id) {
|
|
2244
2453
|
const draft = this.requireReviews();
|
|
@@ -2249,6 +2458,7 @@ var PlanBuilder = class {
|
|
|
2249
2458
|
this.setValue(key, locale, comment.suggestion);
|
|
2250
2459
|
this.recordSource(key, locale, true);
|
|
2251
2460
|
draft.resolve(id);
|
|
2461
|
+
this.reviewEdited(draft, "apply-suggestion", id);
|
|
2252
2462
|
}
|
|
2253
2463
|
resolveReview(id) {
|
|
2254
2464
|
const draft = this.requireReviews();
|
|
@@ -2256,6 +2466,7 @@ var PlanBuilder = class {
|
|
|
2256
2466
|
if (!found) throw errors.X18N5004({ id });
|
|
2257
2467
|
this.touched.add(found.key);
|
|
2258
2468
|
draft.resolve(id);
|
|
2469
|
+
this.reviewEdited(draft, "resolve-review", id);
|
|
2259
2470
|
}
|
|
2260
2471
|
markFresh(op) {
|
|
2261
2472
|
this.requireReviews();
|
|
@@ -2265,11 +2476,12 @@ var PlanBuilder = class {
|
|
|
2265
2476
|
for (const locale of locales) this.recordSource(op.key, locale, true);
|
|
2266
2477
|
}
|
|
2267
2478
|
set(op) {
|
|
2268
|
-
|
|
2479
|
+
const replace = op.replace !== false;
|
|
2480
|
+
if (!this.setValue(op.key, op.locale, op.value, replace)) return;
|
|
2269
2481
|
if (op.locale === this.state.sourceLocale) this.plannedSource.set(op.key, op.value);
|
|
2270
2482
|
else if (op.value !== "") this.recordSource(op.key, op.locale);
|
|
2271
2483
|
if (!this.config.write.keepFilled) return;
|
|
2272
|
-
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);
|
|
2273
2485
|
}
|
|
2274
2486
|
run(ops) {
|
|
2275
2487
|
for (const op of ops) switch (op.type) {
|
|
@@ -2415,6 +2627,7 @@ var PlanBuilder = class {
|
|
|
2415
2627
|
});
|
|
2416
2628
|
texts.set(path, text);
|
|
2417
2629
|
}
|
|
2630
|
+
this.reviewWrites(changes, texts);
|
|
2418
2631
|
const review = this.reviewChange();
|
|
2419
2632
|
if (review) {
|
|
2420
2633
|
changes.push(review);
|
|
@@ -2425,6 +2638,41 @@ var PlanBuilder = class {
|
|
|
2425
2638
|
texts
|
|
2426
2639
|
};
|
|
2427
2640
|
}
|
|
2641
|
+
/** Runs the `reviewEdit` hooks and puts their writes into the changes. */
|
|
2642
|
+
reviewWrites(changes, texts) {
|
|
2643
|
+
const hooks = this.options.registry.plugins.flatMap((plugin) => plugin.reviewEdit ? [plugin.reviewEdit] : []);
|
|
2644
|
+
if (hooks.length === 0 || this.reviewEdits.length === 0) return;
|
|
2645
|
+
const written = /* @__PURE__ */ new Map();
|
|
2646
|
+
const text = (path) => {
|
|
2647
|
+
if (written.has(path)) return written.get(path);
|
|
2648
|
+
const change = changes.find((c) => c.path === path);
|
|
2649
|
+
if (!change) return this.options.text(path);
|
|
2650
|
+
return change.type === "create" ? change.content : change.type === "edit" ? applyTextEdits(texts.get(path) ?? "", change.edits) : void 0;
|
|
2651
|
+
};
|
|
2652
|
+
const ctx = {
|
|
2653
|
+
message: this.options.message,
|
|
2654
|
+
resource: (id) => this.stateById.get(id)?.resource,
|
|
2655
|
+
text
|
|
2656
|
+
};
|
|
2657
|
+
for (const edit of this.reviewEdits) for (const hook of hooks) for (const write of hook(edit, ctx) ?? []) written.set(write.path, write.content);
|
|
2658
|
+
for (const [path, content] of written) {
|
|
2659
|
+
const index = changes.findIndex((c) => c.path === path);
|
|
2660
|
+
const before = index === -1 ? this.options.text(path) : texts.get(path);
|
|
2661
|
+
if (index === -1 && before === content) continue;
|
|
2662
|
+
const change = before === void 0 ? {
|
|
2663
|
+
type: "create",
|
|
2664
|
+
path,
|
|
2665
|
+
content
|
|
2666
|
+
} : {
|
|
2667
|
+
type: "edit",
|
|
2668
|
+
path,
|
|
2669
|
+
edits: diffText(before, content)
|
|
2670
|
+
};
|
|
2671
|
+
if (index === -1) changes.push(change);
|
|
2672
|
+
else changes[index] = change;
|
|
2673
|
+
texts.set(path, before);
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2428
2676
|
reviewChange() {
|
|
2429
2677
|
const reviews = this.state.reviews;
|
|
2430
2678
|
if (!reviews || !this.draft?.changed) return void 0;
|
|
@@ -2443,6 +2691,7 @@ var PlanBuilder = class {
|
|
|
2443
2691
|
};
|
|
2444
2692
|
function createPlanner(options) {
|
|
2445
2693
|
const texts = /* @__PURE__ */ new Map();
|
|
2694
|
+
let textsRevision;
|
|
2446
2695
|
/** Gives new reviews an id and a time, so planning again (after decisions) writes the same comments. */
|
|
2447
2696
|
function withReviewIds(ops, data) {
|
|
2448
2697
|
const used = /* @__PURE__ */ new Set();
|
|
@@ -2506,6 +2755,10 @@ function createPlanner(options) {
|
|
|
2506
2755
|
return {
|
|
2507
2756
|
plan(ops, answers = {}) {
|
|
2508
2757
|
const result = planWith(ops, answers);
|
|
2758
|
+
if (result.plan.revision !== textsRevision) {
|
|
2759
|
+
texts.clear();
|
|
2760
|
+
textsRevision = result.plan.revision;
|
|
2761
|
+
}
|
|
2509
2762
|
texts.set(result.plan.id, result.texts);
|
|
2510
2763
|
return result.plan;
|
|
2511
2764
|
},
|
|
@@ -2748,80 +3001,6 @@ function renderPrompt(data) {
|
|
|
2748
3001
|
return out.join("\n");
|
|
2749
3002
|
}
|
|
2750
3003
|
//#endregion
|
|
2751
|
-
//#region src/project/unused.ts
|
|
2752
|
-
/**
|
|
2753
|
-
* True when a fully dynamic usage (`t(key)`) may reach `key`: the key lies in the usage's scope namespace and under its
|
|
2754
|
-
* scope key prefix. Undefined for a usage without a scope, which may reach any key.
|
|
2755
|
-
*/
|
|
2756
|
-
function scopeMatcher(usage, namespaced) {
|
|
2757
|
-
const namespace = namespaced ? usage.scopeNamespace : void 0;
|
|
2758
|
-
const prefix = usage.scopePrefix;
|
|
2759
|
-
if (namespace === void 0 && !prefix) return void 0;
|
|
2760
|
-
return (key) => {
|
|
2761
|
-
const id = splitKey(key, namespaced);
|
|
2762
|
-
if (namespace !== void 0 && id.namespace !== namespace) return false;
|
|
2763
|
-
if (!prefix) return true;
|
|
2764
|
-
const path = id.path.join(".");
|
|
2765
|
-
return path === prefix || path.startsWith(`${prefix}.`);
|
|
2766
|
-
};
|
|
2767
|
-
}
|
|
2768
|
-
function planUnusedDeletion(options, query = {}) {
|
|
2769
|
-
const ignored = keyMatcher(options.ignoreKeys);
|
|
2770
|
-
const match = query.glob ? keyMatcher([query.glob]) : void 0;
|
|
2771
|
-
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();
|
|
2772
|
-
const scoped = [];
|
|
2773
|
-
const unscoped = [];
|
|
2774
|
-
for (const usage of options.usages) {
|
|
2775
|
-
if (usage.dynamic !== "unknown") continue;
|
|
2776
|
-
const reaches = scopeMatcher(usage, options.namespaced);
|
|
2777
|
-
if (reaches) scoped.push({
|
|
2778
|
-
usage,
|
|
2779
|
-
reaches
|
|
2780
|
-
});
|
|
2781
|
-
else unscoped.push(usage);
|
|
2782
|
-
}
|
|
2783
|
-
const keys = [];
|
|
2784
|
-
const kept = [];
|
|
2785
|
-
for (const key of candidates) {
|
|
2786
|
-
const usages = scoped.filter((s) => s.reaches(key)).map((s) => s.usage);
|
|
2787
|
-
if (usages.length > 0) {
|
|
2788
|
-
kept.push({
|
|
2789
|
-
key,
|
|
2790
|
-
usages
|
|
2791
|
-
});
|
|
2792
|
-
continue;
|
|
2793
|
-
}
|
|
2794
|
-
keys.push({
|
|
2795
|
-
key,
|
|
2796
|
-
messages: options.locations(key).filter((node) => node.leaf).map((node) => ({
|
|
2797
|
-
locale: node.locale,
|
|
2798
|
-
path: node.path
|
|
2799
|
-
}))
|
|
2800
|
-
});
|
|
2801
|
-
}
|
|
2802
|
-
const plan = options.plan(keys.map(({ key }) => ({
|
|
2803
|
-
type: "delete",
|
|
2804
|
-
key,
|
|
2805
|
-
locales: "all"
|
|
2806
|
-
})));
|
|
2807
|
-
if (unscoped.length === 0 || keys.length === 0) return {
|
|
2808
|
-
plan,
|
|
2809
|
-
keys,
|
|
2810
|
-
kept
|
|
2811
|
-
};
|
|
2812
|
-
const files = [...new Set(unscoped.map((usage) => usage.path))].sort();
|
|
2813
|
-
const shown = files.length > 5 ? `${files.slice(0, 5).join(", ")} and ${files.length - 5} more files` : files.join(", ");
|
|
2814
|
-
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.`;
|
|
2815
|
-
return {
|
|
2816
|
-
plan: {
|
|
2817
|
-
...plan,
|
|
2818
|
-
warnings: [...plan.warnings, warning]
|
|
2819
|
-
},
|
|
2820
|
-
keys,
|
|
2821
|
-
kept
|
|
2822
|
-
};
|
|
2823
|
-
}
|
|
2824
|
-
//#endregion
|
|
2825
3004
|
//#region src/project/project.ts
|
|
2826
3005
|
const ioConcurrency = 64;
|
|
2827
3006
|
async function forEachLimit(items, limit, fn) {
|
|
@@ -2861,8 +3040,10 @@ function createProject(options) {
|
|
|
2861
3040
|
const { basesOf, formOf } = derivedKeys(config.usage.derivedKeys);
|
|
2862
3041
|
const scanSignature = hashText(JSON.stringify({
|
|
2863
3042
|
usage: config.usage,
|
|
3043
|
+
extract: config.extract,
|
|
2864
3044
|
namespace: config.namespace,
|
|
2865
3045
|
scanners: registry.scanners.map((s) => s.id),
|
|
3046
|
+
version,
|
|
2866
3047
|
v: 1
|
|
2867
3048
|
}));
|
|
2868
3049
|
const localeFilter = (code) => (!config.locales || config.locales.includes(code)) && !config.ignoreLocales.includes(code);
|
|
@@ -3440,8 +3621,14 @@ function createProject(options) {
|
|
|
3440
3621
|
basesOf,
|
|
3441
3622
|
formOf,
|
|
3442
3623
|
localeInfo,
|
|
3443
|
-
text: (path) => fileTexts.get(path) ?? files.get(path)?.text
|
|
3624
|
+
text: (path) => fileTexts.get(path) ?? files.get(path)?.text,
|
|
3625
|
+
namespaced: () => namespaced
|
|
3444
3626
|
});
|
|
3627
|
+
let lastAnalysis;
|
|
3628
|
+
function analyze() {
|
|
3629
|
+
if (lastAnalysis?.revision !== revision) lastAnalysis = analyzer.analyze();
|
|
3630
|
+
return lastAnalysis;
|
|
3631
|
+
}
|
|
3445
3632
|
function keys(query = {}) {
|
|
3446
3633
|
const { messages, keyPaths } = idx();
|
|
3447
3634
|
const match = query.glob ? keyMatcher([query.glob], config.keySeparator) : void 0;
|
|
@@ -3456,7 +3643,7 @@ function createProject(options) {
|
|
|
3456
3643
|
});
|
|
3457
3644
|
}
|
|
3458
3645
|
if (query.unused) {
|
|
3459
|
-
const unused = new Set(
|
|
3646
|
+
const unused = new Set(analyze().unused);
|
|
3460
3647
|
list = list.filter((key) => unused.has(key));
|
|
3461
3648
|
}
|
|
3462
3649
|
return list.sort();
|
|
@@ -3484,6 +3671,24 @@ function createProject(options) {
|
|
|
3484
3671
|
}
|
|
3485
3672
|
return found.sort((a, b) => b.score - a.score || a.value.length - b.value.length || a.key.localeCompare(b.key));
|
|
3486
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
|
+
}
|
|
3487
3692
|
function get(key) {
|
|
3488
3693
|
const { messages, keyPaths } = idx();
|
|
3489
3694
|
const byLocale = messages.get(key);
|
|
@@ -3498,7 +3703,7 @@ function createProject(options) {
|
|
|
3498
3703
|
messages: list,
|
|
3499
3704
|
missing: locales.filter((locale) => !byLocale.has(locale) && needs(key, locale)),
|
|
3500
3705
|
usages: usageMatches({ key }),
|
|
3501
|
-
diagnostics:
|
|
3706
|
+
diagnostics: diagnosticsOf(key),
|
|
3502
3707
|
reviews: reviews({ key })
|
|
3503
3708
|
};
|
|
3504
3709
|
}
|
|
@@ -3720,7 +3925,7 @@ function createProject(options) {
|
|
|
3720
3925
|
...h,
|
|
3721
3926
|
path: p
|
|
3722
3927
|
}))),
|
|
3723
|
-
analyze
|
|
3928
|
+
analyze,
|
|
3724
3929
|
async loadHistory(historyOptions = {}) {
|
|
3725
3930
|
await ready;
|
|
3726
3931
|
await history.load(historyOptions);
|
|
@@ -3743,13 +3948,18 @@ function createProject(options) {
|
|
|
3743
3948
|
owns: (path) => fileTexts.has(path) || files.has(path) || usageEligible(path),
|
|
3744
3949
|
plan: (ops) => planner.plan(ops),
|
|
3745
3950
|
planUnused: (query) => planUnusedDeletion({
|
|
3746
|
-
unused:
|
|
3951
|
+
unused: analyze().unused,
|
|
3747
3952
|
usages: idx().usages,
|
|
3748
3953
|
namespaced,
|
|
3749
3954
|
ignoreKeys: config.ignoreKeys,
|
|
3750
3955
|
locations: (key) => project.locations(key),
|
|
3751
3956
|
plan: (ops) => planner.plan(ops)
|
|
3752
3957
|
}, query),
|
|
3958
|
+
planFixes: (diagnostics, query) => planFixes(diagnostics, {
|
|
3959
|
+
usages: idx().usages,
|
|
3960
|
+
namespaced,
|
|
3961
|
+
plan: (ops) => planner.plan(ops)
|
|
3962
|
+
}, query),
|
|
3753
3963
|
apply: (plan, applyOptions) => planner.apply(plan, applyOptions?.answers ?? {}),
|
|
3754
3964
|
update,
|
|
3755
3965
|
watch() {
|
|
@@ -3832,7 +4042,12 @@ function defaultDraft() {
|
|
|
3832
4042
|
};
|
|
3833
4043
|
}
|
|
3834
4044
|
function serializablePart(config) {
|
|
3835
|
-
const { presets, plugins, pluginConfigs, history, ...
|
|
4045
|
+
const { presets, plugins, pluginConfigs, history, ignoreKeys: patterns, ...fields } = config;
|
|
4046
|
+
const ignoreKeys = patterns?.filter((pattern) => !(pattern instanceof RegExp));
|
|
4047
|
+
const rest = ignoreKeys === void 0 ? fields : {
|
|
4048
|
+
...fields,
|
|
4049
|
+
ignoreKeys
|
|
4050
|
+
};
|
|
3836
4051
|
if (typeof history !== "object") return {
|
|
3837
4052
|
...rest,
|
|
3838
4053
|
history
|
package/package.json
CHANGED