@x18n/core 0.0.14 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +170 -1
- package/dist/index.mjs +486 -18
- package/package.json +1 -1
- package/schema/config.json +26 -0
- package/schema/reviews.json +98 -0
package/dist/index.d.mts
CHANGED
|
@@ -139,6 +139,31 @@ declare const errorDefinitions: {
|
|
|
139
139
|
}) => string;
|
|
140
140
|
fix: string;
|
|
141
141
|
};
|
|
142
|
+
X18N5001: {
|
|
143
|
+
why: (p: {
|
|
144
|
+
path: string;
|
|
145
|
+
reason: string;
|
|
146
|
+
}) => string;
|
|
147
|
+
fix: string;
|
|
148
|
+
};
|
|
149
|
+
X18N5003: {
|
|
150
|
+
why: (p: {
|
|
151
|
+
reason: string;
|
|
152
|
+
}) => string;
|
|
153
|
+
fix: string;
|
|
154
|
+
};
|
|
155
|
+
X18N5004: {
|
|
156
|
+
why: (p: {
|
|
157
|
+
id: string;
|
|
158
|
+
}) => string;
|
|
159
|
+
fix: string;
|
|
160
|
+
};
|
|
161
|
+
X18N5005: {
|
|
162
|
+
why: (p: {
|
|
163
|
+
id: string;
|
|
164
|
+
}) => string;
|
|
165
|
+
fix: string;
|
|
166
|
+
};
|
|
142
167
|
};
|
|
143
168
|
type ErrorCode = keyof typeof errorDefinitions;
|
|
144
169
|
type X18nError = Diagnostic$1 & {
|
|
@@ -159,6 +184,70 @@ interface ErrorInfo {
|
|
|
159
184
|
export declare function isX18nError(error: unknown): error is X18nError;
|
|
160
185
|
export declare function toErrorInfo(error: unknown): ErrorInfo;
|
|
161
186
|
//#endregion
|
|
187
|
+
//#region src/types/review.d.ts
|
|
188
|
+
type ReviewType = "comment" | "approve" | "request-change" | "suggestion";
|
|
189
|
+
interface ReviewAuthor {
|
|
190
|
+
readonly name?: string | undefined;
|
|
191
|
+
readonly email?: string | undefined;
|
|
192
|
+
}
|
|
193
|
+
/** One entry of a review thread, as stored. */
|
|
194
|
+
interface ReviewComment {
|
|
195
|
+
/** Short random id, unique within the review file. */
|
|
196
|
+
readonly id: string;
|
|
197
|
+
readonly type: ReviewType;
|
|
198
|
+
readonly author?: ReviewAuthor | undefined;
|
|
199
|
+
/** ISO 8601 time. */
|
|
200
|
+
readonly time: string;
|
|
201
|
+
readonly text?: string | undefined;
|
|
202
|
+
/** The proposed value of a `suggestion`. */
|
|
203
|
+
readonly suggestion?: string | undefined;
|
|
204
|
+
/** The value an `approve` approved; the approval is stale once the message has another value. */
|
|
205
|
+
readonly value?: string | undefined;
|
|
206
|
+
readonly resolved?: boolean | undefined;
|
|
207
|
+
}
|
|
208
|
+
/** Everything a review store holds. */
|
|
209
|
+
interface ReviewData {
|
|
210
|
+
/** Comments per key and locale, oldest first. */
|
|
211
|
+
readonly threads: {
|
|
212
|
+
readonly [key: KeyString]: {
|
|
213
|
+
readonly [locale: LocaleCode]: readonly ReviewComment[];
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
/** Hash of the source value when x18n last wrote each target message. */
|
|
217
|
+
readonly sources: {
|
|
218
|
+
readonly [key: KeyString]: {
|
|
219
|
+
readonly [locale: LocaleCode]: string;
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/** Reads and writes the review file (`review.file`). Core does the I/O and puts writes into edit plans. */
|
|
224
|
+
interface ReviewStore {
|
|
225
|
+
readonly id: string;
|
|
226
|
+
/** Throws on invalid content; core reports it as a coded error. */
|
|
227
|
+
readonly parse: (text: string) => ReviewData;
|
|
228
|
+
readonly serialize: (data: ReviewData) => string;
|
|
229
|
+
}
|
|
230
|
+
/** Where a thread stands: the last open approval or change request decides, then open suggestions, then open comments. */
|
|
231
|
+
type ReviewState = "changes-requested" | "approved" | "stale-approval" | "suggested" | "commented";
|
|
232
|
+
interface ThreadComment extends ReviewComment {
|
|
233
|
+
/** An approval of a value the message no longer has. */
|
|
234
|
+
readonly stale: boolean;
|
|
235
|
+
}
|
|
236
|
+
/** The comments on one message. */
|
|
237
|
+
interface ReviewThread {
|
|
238
|
+
readonly key: KeyString;
|
|
239
|
+
readonly locale: LocaleCode;
|
|
240
|
+
readonly comments: readonly ThreadComment[];
|
|
241
|
+
/** Undefined when every comment is resolved. */
|
|
242
|
+
readonly state: ReviewState | undefined;
|
|
243
|
+
}
|
|
244
|
+
interface ReviewFilter {
|
|
245
|
+
readonly key?: KeyString | undefined;
|
|
246
|
+
readonly locale?: LocaleCode | undefined;
|
|
247
|
+
/** Only unresolved comments, and only threads that have some. */
|
|
248
|
+
readonly open?: boolean | undefined;
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
162
251
|
//#region src/types/model.d.ts
|
|
163
252
|
/** A locale code exactly as written in paths and files: `en`, `zh_CN`, `pt-BR`. Never normalized. */
|
|
164
253
|
type LocaleCode = string;
|
|
@@ -277,6 +366,27 @@ type EditOp = {
|
|
|
277
366
|
} | {
|
|
278
367
|
readonly type: "format";
|
|
279
368
|
readonly paths?: readonly string[];
|
|
369
|
+
} | {
|
|
370
|
+
readonly type: "review";
|
|
371
|
+
readonly key: KeyString;
|
|
372
|
+
readonly locale: LocaleCode;
|
|
373
|
+
readonly review: ReviewType;
|
|
374
|
+
readonly text?: string | undefined;
|
|
375
|
+
/** The proposed value, required for `suggestion`. */
|
|
376
|
+
readonly suggestion?: string | undefined;
|
|
377
|
+
readonly author?: ReviewAuthor | undefined;
|
|
378
|
+
readonly id?: string | undefined;
|
|
379
|
+
readonly time?: string | undefined;
|
|
380
|
+
} | {
|
|
381
|
+
readonly type: "resolve-review";
|
|
382
|
+
readonly id: string;
|
|
383
|
+
} | {
|
|
384
|
+
readonly type: "apply-suggestion";
|
|
385
|
+
readonly id: string;
|
|
386
|
+
} | {
|
|
387
|
+
readonly type: "mark-fresh";
|
|
388
|
+
readonly key: KeyString;
|
|
389
|
+
readonly locales?: readonly LocaleCode[] | undefined;
|
|
280
390
|
};
|
|
281
391
|
interface ExtractItem {
|
|
282
392
|
readonly path: string;
|
|
@@ -393,6 +503,8 @@ interface Host {
|
|
|
393
503
|
readonly watch?: ((patterns: readonly string[], onChange: (events: readonly FileEvent[]) => void) => Disposable) | undefined;
|
|
394
504
|
readonly cache?: CacheStore | undefined;
|
|
395
505
|
readonly logger?: Logger | undefined;
|
|
506
|
+
/** Who writes reviews. The node host reads git `user.name` and `user.email`. */
|
|
507
|
+
readonly identity?: (() => Promise<ReviewAuthor | undefined>) | undefined;
|
|
396
508
|
}
|
|
397
509
|
//#endregion
|
|
398
510
|
//#region src/types/plugin.d.ts
|
|
@@ -646,6 +758,7 @@ interface CheckContext {
|
|
|
646
758
|
}>;
|
|
647
759
|
/** When a message last changed, from the history providers. Undefined until history is loaded. */
|
|
648
760
|
readonly history: (key: KeyString, locale: LocaleCode) => MessageChange | undefined;
|
|
761
|
+
readonly reviews: (filter?: ReviewFilter) => readonly ReviewThread[];
|
|
649
762
|
}
|
|
650
763
|
interface Check {
|
|
651
764
|
readonly id: string;
|
|
@@ -741,6 +854,8 @@ interface Plugin {
|
|
|
741
854
|
readonly reporters?: readonly Reporter[] | undefined;
|
|
742
855
|
readonly placeholders?: readonly PlaceholderSyntax[] | undefined;
|
|
743
856
|
readonly histories?: readonly HistoryProvider[] | undefined;
|
|
857
|
+
/** Only one store is active: the last one, so a plugin's store replaces the built-in one. */
|
|
858
|
+
readonly reviewStores?: readonly ReviewStore[] | undefined;
|
|
744
859
|
/** Turns raw key text into canonical form. The first plugin that returns a value wins. */
|
|
745
860
|
readonly normalizeKey?: ((raw: string, ctx: KeyContext) => string | undefined) | undefined;
|
|
746
861
|
readonly derivedKeys?: readonly string[] | undefined;
|
|
@@ -902,6 +1017,14 @@ interface NamespaceOptions {
|
|
|
902
1017
|
/** How `{namespaces}` folders name a namespace: `dot` makes `pages/home` into `pages.home`, `slash` keeps `pages/home`, `camel` makes `todos-page` into `todosPage`. Default `dot`. */
|
|
903
1018
|
readonly fromPath?: "dot" | "slash" | "camel" | undefined;
|
|
904
1019
|
}
|
|
1020
|
+
interface ReviewOptions {
|
|
1021
|
+
/** Default true. */
|
|
1022
|
+
readonly enabled?: boolean | undefined;
|
|
1023
|
+
/** Review file, relative to the project root. Default `.x18n/reviews.yml`. */
|
|
1024
|
+
readonly file?: string | undefined;
|
|
1025
|
+
/** Author of new reviews, instead of the host identity (git `user.name` and `user.email`). */
|
|
1026
|
+
readonly author?: ReviewAuthor | undefined;
|
|
1027
|
+
}
|
|
905
1028
|
type NamespaceFromPath = NonNullable<NamespaceOptions["fromPath"]>;
|
|
906
1029
|
type PluginSpecifier = string | readonly [string, JsonObject];
|
|
907
1030
|
/** The part of the config that YAML and JSON files can hold. Validated by `ConfigSchema`. */
|
|
@@ -943,6 +1066,8 @@ interface SerializableConfig {
|
|
|
943
1066
|
readonly glossary?: readonly GlossaryEntry[] | undefined;
|
|
944
1067
|
/** 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`). */
|
|
945
1068
|
readonly history?: boolean | HistoryOptions | undefined;
|
|
1069
|
+
/** Review comments, approvals and suggested values on messages, kept in a file tracked in git. */
|
|
1070
|
+
readonly review?: ReviewOptions | undefined;
|
|
946
1071
|
/** Monorepo: directories (globs) that are projects inheriting this config. */
|
|
947
1072
|
readonly projects?: readonly string[] | undefined;
|
|
948
1073
|
}
|
|
@@ -1018,6 +1143,11 @@ interface ResolvedConfig {
|
|
|
1018
1143
|
readonly ignoreKeys: readonly string[];
|
|
1019
1144
|
readonly glossary: readonly GlossaryEntry[];
|
|
1020
1145
|
readonly history: false | HistorySettings;
|
|
1146
|
+
readonly review: {
|
|
1147
|
+
readonly enabled: boolean;
|
|
1148
|
+
readonly file: string;
|
|
1149
|
+
readonly author: ReviewAuthor | undefined;
|
|
1150
|
+
};
|
|
1021
1151
|
readonly pluginConfigs: Partial<PluginConfigs>;
|
|
1022
1152
|
/** Origin of each top-level value, for `explain()`. */
|
|
1023
1153
|
readonly origins: {
|
|
@@ -1033,6 +1163,10 @@ export declare function definePlugin<O, const T extends Plugin>(factory: (option
|
|
|
1033
1163
|
export declare function definePreset<const T extends Preset>(preset: T): T;
|
|
1034
1164
|
export declare function defineConfig(config: UserConfig & { readonly [K in keyof PluginConfigs]?: PluginConfigs[K]; }): UserConfig;
|
|
1035
1165
|
//#endregion
|
|
1166
|
+
//#region src/fuzzy.d.ts
|
|
1167
|
+
/** Subsequence match score of `query` in `text`, case-insensitive; higher is better, -1 when it does not match. Substring hits rank above every subsequence. */
|
|
1168
|
+
export declare function fuzzyScore(query: string, text: string): number;
|
|
1169
|
+
//#endregion
|
|
1036
1170
|
//#region src/keys.d.ts
|
|
1037
1171
|
/** Canonical string form of a key: `ns:a.b.c`, or `a.b.c` without namespace. */
|
|
1038
1172
|
export declare function joinKey(namespace: string | undefined, path: readonly string[]): KeyString;
|
|
@@ -1143,6 +1277,7 @@ interface PluginRegistry {
|
|
|
1143
1277
|
readonly manifests: readonly ManifestReader[];
|
|
1144
1278
|
readonly extractors: readonly Extractor[];
|
|
1145
1279
|
readonly histories: readonly HistoryProvider[];
|
|
1280
|
+
readonly reviewStores: readonly ReviewStore[];
|
|
1146
1281
|
readonly conflicts: readonly CapabilityConflict[];
|
|
1147
1282
|
/** The loaded format for a file path: config `formats` first, then the first format that claims the extension. */
|
|
1148
1283
|
readonly formatFor: (path: string, hint?: string) => Format | undefined;
|
|
@@ -1265,6 +1400,18 @@ interface KeyQuery {
|
|
|
1265
1400
|
/** Only keys without usages. */
|
|
1266
1401
|
readonly unused?: boolean | undefined;
|
|
1267
1402
|
}
|
|
1403
|
+
interface SearchOptions {
|
|
1404
|
+
/** Only values containing the query; by default a fuzzy (subsequence) match also counts. Both ignore case. */
|
|
1405
|
+
readonly exact?: boolean | undefined;
|
|
1406
|
+
}
|
|
1407
|
+
/** A key found by its value: the best-matching locale, source first on ties. */
|
|
1408
|
+
interface SearchMatch {
|
|
1409
|
+
readonly key: KeyString;
|
|
1410
|
+
readonly locale: LocaleCode;
|
|
1411
|
+
readonly value: string;
|
|
1412
|
+
/** Higher is better; substring hits rank above fuzzy ones. */
|
|
1413
|
+
readonly score: number;
|
|
1414
|
+
}
|
|
1268
1415
|
interface UsageMatch {
|
|
1269
1416
|
readonly usage: Usage;
|
|
1270
1417
|
/** True for prefix usages that may refer to the key. */
|
|
@@ -1285,6 +1432,7 @@ interface KeyInfo {
|
|
|
1285
1432
|
readonly missing: readonly LocaleCode[];
|
|
1286
1433
|
readonly usages: readonly UsageMatch[];
|
|
1287
1434
|
readonly diagnostics: readonly Diagnostic[];
|
|
1435
|
+
readonly reviews: readonly ReviewThread[];
|
|
1288
1436
|
}
|
|
1289
1437
|
/** The key, or the resource node, under a position. */
|
|
1290
1438
|
type KeyTarget = {
|
|
@@ -1417,6 +1565,8 @@ interface Project {
|
|
|
1417
1565
|
/** Extraction templates: config `extract.templates` first, then those of presets. */
|
|
1418
1566
|
readonly extractTemplates: readonly ExtractTemplate[];
|
|
1419
1567
|
readonly keys: (query?: KeyQuery) => KeyString[];
|
|
1568
|
+
/** Keys whose value in some locale matches the query, best first. */
|
|
1569
|
+
readonly search: (query: string, options?: SearchOptions) => SearchMatch[];
|
|
1420
1570
|
readonly get: (key: KeyString) => KeyInfo | undefined;
|
|
1421
1571
|
readonly message: (key: KeyString, locale: LocaleCode) => Message | undefined;
|
|
1422
1572
|
/** The message a usage of `key` shows: the key's own, or one of its derived keys (`item` → `item_other`). */
|
|
@@ -1443,6 +1593,12 @@ interface Project {
|
|
|
1443
1593
|
readonly history: (key: KeyString, locale: LocaleCode) => MessageChange | undefined;
|
|
1444
1594
|
/** The value a message had at a time (ms), from the provider that knows the message. */
|
|
1445
1595
|
readonly valueAt: (key: KeyString, locale: LocaleCode, at: number) => MessageLeaf | undefined;
|
|
1596
|
+
/** Review threads, in key order and then locale order. */
|
|
1597
|
+
readonly reviews: (filter?: ReviewFilter) => readonly ReviewThread[];
|
|
1598
|
+
/** The review file when reviews are on and a review store is registered. */
|
|
1599
|
+
readonly reviewFile: string | undefined;
|
|
1600
|
+
/** Author of new reviews: config `review.author`, else the host identity. */
|
|
1601
|
+
readonly reviewAuthor: () => Promise<ReviewAuthor | undefined>;
|
|
1446
1602
|
/** Parts of a translate or review prompt for an agent or LLM; render with `renderPrompt`. */
|
|
1447
1603
|
readonly prompt: (kind: "translate" | "review", locale: LocaleCode, options?: PromptOptions) => PromptData;
|
|
1448
1604
|
readonly explain: () => ProjectExplanation;
|
|
@@ -1484,6 +1640,10 @@ type PlaceholderComparer = (source: string, target: string) => {
|
|
|
1484
1640
|
export declare function createPlaceholderComparer(syntaxes: readonly PlaceholderSyntax[], find?: (value: string) => readonly Placeholder[]): PlaceholderComparer;
|
|
1485
1641
|
export declare function placeholderProblems(project: Project, ops: readonly EditOp[]): PlaceholderProblem[];
|
|
1486
1642
|
//#endregion
|
|
1643
|
+
//#region src/project/explain.d.ts
|
|
1644
|
+
/** `project.explain()` as the text `x18n doctor` prints. `root` and `configFile` are shown as given. */
|
|
1645
|
+
export declare function renderExplanation(e: ProjectExplanation, configFile?: string | undefined): string;
|
|
1646
|
+
//#endregion
|
|
1487
1647
|
//#region src/project/project.d.ts
|
|
1488
1648
|
interface CreateProjectOptions {
|
|
1489
1649
|
readonly host: Host;
|
|
@@ -1542,6 +1702,15 @@ interface PluginFile {
|
|
|
1542
1702
|
} | undefined;
|
|
1543
1703
|
}
|
|
1544
1704
|
export declare const PluginFileSchema: v.GenericSchema<PluginFile>;
|
|
1705
|
+
/** The review file (`.x18n/reviews.yml`), as the YAML review store reads it. */
|
|
1706
|
+
interface ReviewFile {
|
|
1707
|
+
readonly $schema?: string | undefined;
|
|
1708
|
+
/** File format version. */
|
|
1709
|
+
readonly x18n: 1;
|
|
1710
|
+
readonly threads?: ReviewData["threads"] | undefined;
|
|
1711
|
+
readonly sources?: ReviewData["sources"] | undefined;
|
|
1712
|
+
}
|
|
1713
|
+
export declare const ReviewFileSchema: v.GenericSchema<ReviewFile>;
|
|
1545
1714
|
interface SchemaIssue {
|
|
1546
1715
|
readonly path: string;
|
|
1547
1716
|
readonly message: string;
|
|
@@ -1579,4 +1748,4 @@ export declare function rangeContains(range: TextRange, offset: number): boolean
|
|
|
1579
1748
|
/** Fast non-cryptographic string hash (cyrb53), as hex. */
|
|
1580
1749
|
export declare function hashText(text: string): string;
|
|
1581
1750
|
//#endregion
|
|
1582
|
-
export type { Analysis, Answers, ApplyResult, ArgSource, 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, KeyContext, KeyId, KeyInfo, KeyQuery, KeyString, KeyTarget, LazyFormat, LazyScanner, LibraryOptions, LineIndex, LinePosition, LocaleCode, LocaleCoverage, LocaleFinder, LocaleInfo, Location, Logger, ManifestReader, MemoryHost, Message, MessageChange, MessageHistory, MessageLeaf, MessageOp, MessageTree, NamespaceFromPath, NamespaceOptions, NodeRange, OverlayHost, 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, PromptItem, PromptOptions, RegexScannerSpec, ReportInput, ReportSummary, Reporter, ReporterContext, ReporterLoader, ResolveConfigOptions, ResolvedConfig, Resource, ResourceBlock, ResourceCandidate, ResourceId, ResourceNode, ResourceSpec, ResourceTransform, ResourceTransformContext, ScanContext, ScanResult, Scanner, ScannerEntry, SchemaIssue, Scope, SerializableConfig, Setup, Severity, SourceFile, TextEdit, TextRange, TreeLeaf, Usage, UsageCandidate, UsageContext, UsageKind, UsageMatch, UsageOptions, UsageQuery, UserConfig, WireDiagnostic, WireRange, WriteOptions, X18nError };
|
|
1751
|
+
export type { Analysis, Answers, ApplyResult, ArgSource, 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, KeyContext, KeyId, KeyInfo, KeyQuery, KeyString, KeyTarget, LazyFormat, LazyScanner, LibraryOptions, LineIndex, LinePosition, LocaleCode, LocaleCoverage, LocaleFinder, LocaleInfo, Location, Logger, ManifestReader, MemoryHost, Message, MessageChange, MessageHistory, MessageLeaf, MessageOp, MessageTree, NamespaceFromPath, NamespaceOptions, NodeRange, OverlayHost, 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, PromptItem, PromptOptions, RegexScannerSpec, 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, Usage, UsageCandidate, UsageContext, UsageKind, UsageMatch, UsageOptions, UsageQuery, UserConfig, WireDiagnostic, WireRange, WriteOptions, X18nError };
|
package/dist/index.mjs
CHANGED
|
@@ -225,6 +225,22 @@ const errorDefinitions = {
|
|
|
225
225
|
X18N4008: {
|
|
226
226
|
why: (p) => `Invalid edit input: ${p.reason}`,
|
|
227
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 the check reports a problem with 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`."
|
|
228
244
|
}
|
|
229
245
|
};
|
|
230
246
|
/** Raise with `throw errors.X18N2002({ path, reason })`. Core has no reporters: surfaces print what they catch. */
|
|
@@ -251,6 +267,24 @@ function toErrorInfo(error) {
|
|
|
251
267
|
};
|
|
252
268
|
}
|
|
253
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
|
|
254
288
|
//#region src/keys.ts
|
|
255
289
|
/** Canonical string form of a key: `ns:a.b.c`, or `a.b.c` without namespace. */
|
|
256
290
|
function joinKey(namespace, path) {
|
|
@@ -701,6 +735,7 @@ function createRegistry(input, config) {
|
|
|
701
735
|
manifests: plugins.flatMap((plugin) => plugin.manifests ?? []),
|
|
702
736
|
extractors: plugins.flatMap((plugin) => plugin.extractors ?? []),
|
|
703
737
|
histories: plugins.flatMap((plugin) => plugin.histories ?? []),
|
|
738
|
+
reviewStores: plugins.flatMap((plugin) => plugin.reviewStores ?? []),
|
|
704
739
|
conflicts,
|
|
705
740
|
formatById,
|
|
706
741
|
formatIdFor,
|
|
@@ -816,6 +851,10 @@ const namespaceOptions = v.strictObject({
|
|
|
816
851
|
"camel"
|
|
817
852
|
]))
|
|
818
853
|
});
|
|
854
|
+
const reviewAuthor = v.strictObject({
|
|
855
|
+
name: v.optional(v.string()),
|
|
856
|
+
email: v.optional(v.string())
|
|
857
|
+
});
|
|
819
858
|
/** Validates YAML and JSON configs, and the serializable part of TS configs. */
|
|
820
859
|
const ConfigSchema = v.strictObject({
|
|
821
860
|
$schema: v.optional(v.string()),
|
|
@@ -855,6 +894,11 @@ const ConfigSchema = v.strictObject({
|
|
|
855
894
|
ignoreKeys: v.optional(strings),
|
|
856
895
|
glossary: v.optional(v.array(glossaryEntry)),
|
|
857
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
|
+
})),
|
|
858
902
|
projects: v.optional(strings)
|
|
859
903
|
});
|
|
860
904
|
const PluginFileSchema = v.strictObject({
|
|
@@ -896,6 +940,27 @@ const PluginFileSchema = v.strictObject({
|
|
|
896
940
|
}))),
|
|
897
941
|
extract: v.optional(v.strictObject({ templates: v.optional(extractTemplates) }))
|
|
898
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
|
+
});
|
|
899
964
|
/** Validates data against a schema and returns readable issues instead of throwing. */
|
|
900
965
|
function validate(schema, data) {
|
|
901
966
|
const result = v.safeParse(schema, data);
|
|
@@ -975,6 +1040,28 @@ function placeholderProblems(project, ops) {
|
|
|
975
1040
|
});
|
|
976
1041
|
}
|
|
977
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
|
|
978
1065
|
//#region src/project/history.ts
|
|
979
1066
|
const localChange = { updatedAt: Number.POSITIVE_INFINITY };
|
|
980
1067
|
/** A target message is stale when it last changed before its source message. */
|
|
@@ -1224,7 +1311,7 @@ function computeCoverage(state, options, sourceKeys) {
|
|
|
1224
1311
|
if (value === void 0 || value === "") {
|
|
1225
1312
|
if (inherited(key)) continue;
|
|
1226
1313
|
(value === void 0 ? missing : empty).push(key);
|
|
1227
|
-
} 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);
|
|
1228
1315
|
}
|
|
1229
1316
|
return {
|
|
1230
1317
|
locale,
|
|
@@ -1375,6 +1462,7 @@ function createAnalyzer(options) {
|
|
|
1375
1462
|
localeInfo: options.localeInfo,
|
|
1376
1463
|
text: options.text,
|
|
1377
1464
|
history: state.history,
|
|
1465
|
+
reviews: state.reviews,
|
|
1378
1466
|
*hardStrings() {
|
|
1379
1467
|
for (const [path, file] of state.files) for (const hard of file.result.hardStrings ?? []) yield {
|
|
1380
1468
|
...hard,
|
|
@@ -1402,6 +1490,161 @@ function createAnalyzer(options) {
|
|
|
1402
1490
|
return { analyze };
|
|
1403
1491
|
}
|
|
1404
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
|
|
1405
1648
|
//#region src/project/planner.ts
|
|
1406
1649
|
const safeSegment = /^[\w-]+$/;
|
|
1407
1650
|
const safeFlat = /^[\w.-]+$/;
|
|
@@ -1471,6 +1714,9 @@ var PlanBuilder = class {
|
|
|
1471
1714
|
touched = /* @__PURE__ */ new Set();
|
|
1472
1715
|
stateById;
|
|
1473
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;
|
|
1474
1720
|
constructor(options, state, answers) {
|
|
1475
1721
|
this.options = options;
|
|
1476
1722
|
this.state = state;
|
|
@@ -1797,6 +2043,7 @@ var PlanBuilder = class {
|
|
|
1797
2043
|
const renamed = keys.includes(op.from) ? /* @__PURE__ */ new Set() : this.renameNode(op.from, op.to);
|
|
1798
2044
|
for (const [from, to] of pairs) {
|
|
1799
2045
|
this.moveKey(from, to, renamed);
|
|
2046
|
+
this.reviewDraft()?.move(from, to);
|
|
1800
2047
|
if (op.usages !== false) this.rewriteUsages(from, to);
|
|
1801
2048
|
}
|
|
1802
2049
|
if (op.usages === false) return;
|
|
@@ -1808,6 +2055,7 @@ var PlanBuilder = class {
|
|
|
1808
2055
|
if (keys.length === 0) throw errors.X18N4006({ key: op.key });
|
|
1809
2056
|
for (const key of keys) {
|
|
1810
2057
|
this.touched.add(key);
|
|
2058
|
+
this.reviewDraft()?.remove(key, op.locales ?? "all");
|
|
1811
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));
|
|
1812
2060
|
const used = this.options.usageMatches({ key }).filter((match) => !match.possible).length;
|
|
1813
2061
|
if (used > 0) this.warnings.push(`"${key}" still has ${used} usage${used === 1 ? "" : "s"}.`);
|
|
@@ -1827,13 +2075,75 @@ var PlanBuilder = class {
|
|
|
1827
2075
|
}
|
|
1828
2076
|
}
|
|
1829
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
|
+
}
|
|
1830
2143
|
run(ops) {
|
|
1831
2144
|
for (const op of ops) switch (op.type) {
|
|
1832
2145
|
case "set":
|
|
1833
|
-
this.
|
|
1834
|
-
if (this.config.write.keepFilled) {
|
|
1835
|
-
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, "");
|
|
1836
|
-
}
|
|
2146
|
+
this.set(op);
|
|
1837
2147
|
break;
|
|
1838
2148
|
case "delete":
|
|
1839
2149
|
this.delete(op);
|
|
@@ -1845,7 +2155,19 @@ var PlanBuilder = class {
|
|
|
1845
2155
|
case "extract":
|
|
1846
2156
|
this.extract(op.items);
|
|
1847
2157
|
break;
|
|
1848
|
-
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);
|
|
1849
2171
|
}
|
|
1850
2172
|
}
|
|
1851
2173
|
/** ---- Changes ---- */
|
|
@@ -1954,16 +2276,52 @@ var PlanBuilder = class {
|
|
|
1954
2276
|
});
|
|
1955
2277
|
texts.set(path, text);
|
|
1956
2278
|
}
|
|
2279
|
+
const review = this.reviewChange();
|
|
2280
|
+
if (review) {
|
|
2281
|
+
changes.push(review);
|
|
2282
|
+
texts.set(review.path, this.state.reviews?.text);
|
|
2283
|
+
}
|
|
1957
2284
|
return {
|
|
1958
2285
|
changes,
|
|
1959
2286
|
texts
|
|
1960
2287
|
};
|
|
1961
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
|
+
}
|
|
1962
2304
|
};
|
|
1963
2305
|
function createPlanner(options) {
|
|
1964
2306
|
const texts = /* @__PURE__ */ new Map();
|
|
1965
|
-
|
|
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) {
|
|
1966
2323
|
const state = options.state();
|
|
2324
|
+
const ops = withReviewIds(input, state.reviews?.data ?? noReviews);
|
|
1967
2325
|
const builder = new PlanBuilder(options, state, answers);
|
|
1968
2326
|
builder.run(ops);
|
|
1969
2327
|
const result = builder.changes();
|
|
@@ -2063,7 +2421,8 @@ function buildPrompt(project, kind, to, options, extra) {
|
|
|
2063
2421
|
const from = project.sourceLocale;
|
|
2064
2422
|
const match = typeof options.keys === "string" ? keyMatcher([options.keys]) : void 0;
|
|
2065
2423
|
const list = Array.isArray(options.keys) ? new Set(options.keys) : void 0;
|
|
2066
|
-
const
|
|
2424
|
+
const outdated = new Set(project.analyze().coverage.find((c) => c.locale === to)?.outdated);
|
|
2425
|
+
const stale = (key) => outdated.has(key);
|
|
2067
2426
|
const candidates = project.keys({ namespace: options.namespace }).filter((key) => {
|
|
2068
2427
|
if (match && !match(key)) return false;
|
|
2069
2428
|
if (list && !list.has(key)) return false;
|
|
@@ -2118,7 +2477,11 @@ function buildPrompt(project, kind, to, options, extra) {
|
|
|
2118
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.`,
|
|
2119
2478
|
`Then run \`x18n check --only placeholder-mismatch,invalid-syntax --locale ${to} --json\` and fix the reported keys.`,
|
|
2120
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\`.`
|
|
2121
|
-
] : [`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
|
+
];
|
|
2122
2485
|
return {
|
|
2123
2486
|
kind,
|
|
2124
2487
|
project: project.name,
|
|
@@ -2200,6 +2563,10 @@ function createProject(options) {
|
|
|
2200
2563
|
let usageEligible = () => false;
|
|
2201
2564
|
let resourceIndex;
|
|
2202
2565
|
let resolved = /* @__PURE__ */ new WeakMap();
|
|
2566
|
+
const reviewFile = config.review.file;
|
|
2567
|
+
let reviewText;
|
|
2568
|
+
let reviewData = noReviews;
|
|
2569
|
+
let reviewError;
|
|
2203
2570
|
const normalizers = registry.plugins.flatMap((plugin) => plugin.normalizeKey ? [plugin.normalizeKey] : []);
|
|
2204
2571
|
const transforms = registry.plugins.flatMap((plugin) => plugin.transformResource ? [plugin.transformResource] : []);
|
|
2205
2572
|
const localeInfoHooks = registry.plugins.flatMap((plugin) => plugin.localeInfo ? [plugin.localeInfo] : []);
|
|
@@ -2236,6 +2603,44 @@ function createProject(options) {
|
|
|
2236
2603
|
const form = formOf(key);
|
|
2237
2604
|
return !form || categoriesOf(localeInfo(locale), form.type).includes(form.category);
|
|
2238
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
|
+
}
|
|
2239
2644
|
async function discover() {
|
|
2240
2645
|
const ctx = {
|
|
2241
2646
|
host,
|
|
@@ -2692,7 +3097,10 @@ function createProject(options) {
|
|
|
2692
3097
|
error: s.error,
|
|
2693
3098
|
path
|
|
2694
3099
|
}] : []),
|
|
2695
|
-
discoveryErrors,
|
|
3100
|
+
discoveryErrors: [...discoveryErrors, ...reviewError ? [{
|
|
3101
|
+
error: reviewError,
|
|
3102
|
+
path: reviewFile
|
|
3103
|
+
}] : []],
|
|
2696
3104
|
messages: idx().messages,
|
|
2697
3105
|
parents: idx().parents,
|
|
2698
3106
|
usages: idx().usages,
|
|
@@ -2700,7 +3108,9 @@ function createProject(options) {
|
|
|
2700
3108
|
prefixUsages: idx().prefixUsages,
|
|
2701
3109
|
derived: idx().derived,
|
|
2702
3110
|
files,
|
|
2703
|
-
history: history.get
|
|
3111
|
+
history: history.get,
|
|
3112
|
+
fresh,
|
|
3113
|
+
reviews
|
|
2704
3114
|
}),
|
|
2705
3115
|
basesOf,
|
|
2706
3116
|
formOf,
|
|
@@ -2726,6 +3136,29 @@ function createProject(options) {
|
|
|
2726
3136
|
}
|
|
2727
3137
|
return list.sort();
|
|
2728
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
|
+
}
|
|
2729
3162
|
function get(key) {
|
|
2730
3163
|
const { messages, keyPaths } = idx();
|
|
2731
3164
|
const byLocale = messages.get(key);
|
|
@@ -2739,7 +3172,8 @@ function createProject(options) {
|
|
|
2739
3172
|
messages: list,
|
|
2740
3173
|
missing: locales.filter((locale) => !byLocale.has(locale) && needs(key, locale)),
|
|
2741
3174
|
usages: usageMatches({ key }),
|
|
2742
|
-
diagnostics: analyzer.analyze().diagnostics.filter((d) => d.key === key)
|
|
3175
|
+
diagnostics: analyzer.analyze().diagnostics.filter((d) => d.key === key),
|
|
3176
|
+
reviews: reviews({ key })
|
|
2743
3177
|
};
|
|
2744
3178
|
}
|
|
2745
3179
|
function keyAt(path, offset) {
|
|
@@ -2793,6 +3227,10 @@ function createProject(options) {
|
|
|
2793
3227
|
conflicts: registry.conflicts,
|
|
2794
3228
|
errors: [
|
|
2795
3229
|
...discoveryErrors,
|
|
3230
|
+
...reviewError ? [{
|
|
3231
|
+
error: reviewError,
|
|
3232
|
+
path: reviewFile
|
|
3233
|
+
}] : [],
|
|
2796
3234
|
...resources.flatMap((state) => state.error ? [{
|
|
2797
3235
|
error: state.error,
|
|
2798
3236
|
path: state.resource.path
|
|
@@ -2822,14 +3260,22 @@ function createProject(options) {
|
|
|
2822
3260
|
collect(idx());
|
|
2823
3261
|
return [...out];
|
|
2824
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
|
+
}
|
|
2825
3270
|
let current = Promise.resolve();
|
|
2826
3271
|
async function update(events) {
|
|
2827
3272
|
const run = current.then(async () => {
|
|
2828
3273
|
if (events.length === 0) return void 0;
|
|
2829
3274
|
const before = idx();
|
|
2830
3275
|
const paths = new Set(events.map((event) => event.path));
|
|
3276
|
+
const reviewKeys = paths.has(reviewFile) ? await reloadReviews() : [];
|
|
2831
3277
|
const resourcePaths = new Set([...resourceStates.values()].map((state) => state.resource.path));
|
|
2832
|
-
const maybeResource = (path) => !resourcePaths.has(path) && registry.formatIdFor(path) !== void 0 && !usageEligible(path);
|
|
3278
|
+
const maybeResource = (path) => path !== reviewFile && !resourcePaths.has(path) && registry.formatIdFor(path) !== void 0 && !usageEligible(path);
|
|
2833
3279
|
const blockPaths = new Set([...resourceStates.values()].filter((state) => state.resource.block).map((state) => state.resource.path));
|
|
2834
3280
|
const structural = events.some((event) => event.type !== "change" || maybeResource(event.path) || blockPaths.has(event.path));
|
|
2835
3281
|
if (structural || [...paths].some((path) => resourcePaths.has(path))) await loadResources(structural ? await discover() : [...resourceStates.values()].map((state) => ({
|
|
@@ -2843,7 +3289,7 @@ function createProject(options) {
|
|
|
2843
3289
|
const change = {
|
|
2844
3290
|
revision,
|
|
2845
3291
|
paths: [...paths],
|
|
2846
|
-
keys: touchedKeys(paths, before)
|
|
3292
|
+
keys: [.../* @__PURE__ */ new Set([...touchedKeys(paths, before), ...reviewKeys])]
|
|
2847
3293
|
};
|
|
2848
3294
|
emit(change);
|
|
2849
3295
|
return change;
|
|
@@ -2862,7 +3308,9 @@ function createProject(options) {
|
|
|
2862
3308
|
fallbacks,
|
|
2863
3309
|
namespaced,
|
|
2864
3310
|
resources: [...resourceStates.values()],
|
|
2865
|
-
fileTexts
|
|
3311
|
+
fileTexts,
|
|
3312
|
+
reviews: reviewSource(),
|
|
3313
|
+
reviewProblem: reviewProblem()
|
|
2866
3314
|
}),
|
|
2867
3315
|
message: (key, locale) => message(key, locale),
|
|
2868
3316
|
messages: (key) => idx().messages.get(key),
|
|
@@ -2876,7 +3324,8 @@ function createProject(options) {
|
|
|
2876
3324
|
update
|
|
2877
3325
|
});
|
|
2878
3326
|
const ready = (async () => {
|
|
2879
|
-
|
|
3327
|
+
const candidates = await discover();
|
|
3328
|
+
await Promise.all([loadResources(candidates, "all"), loadReviews()]);
|
|
2880
3329
|
await scanAll();
|
|
2881
3330
|
revision++;
|
|
2882
3331
|
})();
|
|
@@ -2905,6 +3354,7 @@ function createProject(options) {
|
|
|
2905
3354
|
placeholderSyntaxes: registry.placeholders,
|
|
2906
3355
|
extractTemplates: [...(config.extract.templates ?? []).map((template) => typeof template === "string" ? { template } : template), ...registry.extractors.flatMap((extractor) => extractor.templates)],
|
|
2907
3356
|
keys,
|
|
3357
|
+
search,
|
|
2908
3358
|
get,
|
|
2909
3359
|
message,
|
|
2910
3360
|
parseKey(input) {
|
|
@@ -2947,13 +3397,18 @@ function createProject(options) {
|
|
|
2947
3397
|
},
|
|
2948
3398
|
history: history.get,
|
|
2949
3399
|
valueAt: history.valueAt,
|
|
3400
|
+
reviews,
|
|
3401
|
+
reviewFile: reviewStore && reviewFile,
|
|
3402
|
+
async reviewAuthor() {
|
|
3403
|
+
return config.review.author ?? await host.identity?.();
|
|
3404
|
+
},
|
|
2950
3405
|
prompt: (kind, locale, promptOptions = {}) => buildPrompt(project, kind, locale, promptOptions, {
|
|
2951
3406
|
config,
|
|
2952
3407
|
hooks: registry.plugins.flatMap((plugin) => plugin.translatePrompt ? [plugin.translatePrompt] : [])
|
|
2953
3408
|
}),
|
|
2954
3409
|
explain,
|
|
2955
3410
|
localeInfo,
|
|
2956
|
-
text: (path) => fileTexts.get(path) ?? files.get(path)?.text,
|
|
3411
|
+
text: (path) => fileTexts.get(path) ?? files.get(path)?.text ?? (path === reviewFile ? reviewText : void 0),
|
|
2957
3412
|
owns: (path) => fileTexts.has(path) || files.has(path) || usageEligible(path),
|
|
2958
3413
|
plan: (ops) => planner.plan(ops),
|
|
2959
3414
|
apply: (plan, applyOptions) => planner.apply(plan, applyOptions?.answers ?? {}),
|
|
@@ -3027,6 +3482,11 @@ function defaultDraft() {
|
|
|
3027
3482
|
ignoreKeys: [],
|
|
3028
3483
|
glossary: [],
|
|
3029
3484
|
history: { providers: [] },
|
|
3485
|
+
review: {
|
|
3486
|
+
enabled: true,
|
|
3487
|
+
file: ".x18n/reviews.yml",
|
|
3488
|
+
author: void 0
|
|
3489
|
+
},
|
|
3030
3490
|
pluginConfigs: {}
|
|
3031
3491
|
};
|
|
3032
3492
|
}
|
|
@@ -3154,6 +3614,14 @@ function applyLayer(draft, layer, origin, origins) {
|
|
|
3154
3614
|
};
|
|
3155
3615
|
mark("history");
|
|
3156
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
|
+
}
|
|
3157
3625
|
if (layer.pluginConfigs !== void 0) {
|
|
3158
3626
|
draft.pluginConfigs = {
|
|
3159
3627
|
...draft.pluginConfigs,
|
|
@@ -3311,4 +3779,4 @@ async function resolveConfig(options) {
|
|
|
3311
3779
|
};
|
|
3312
3780
|
}
|
|
3313
3781
|
//#endregion
|
|
3314
|
-
export { ConfigSchema, PluginFileSchema, applyMessageOps, applyTextEdits, buildTree, contractVersion, createLibraryPlugin, 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 };
|
package/package.json
CHANGED
package/schema/config.json
CHANGED
|
@@ -591,6 +591,32 @@
|
|
|
591
591
|
}
|
|
592
592
|
]
|
|
593
593
|
},
|
|
594
|
+
"review": {
|
|
595
|
+
"type": "object",
|
|
596
|
+
"properties": {
|
|
597
|
+
"enabled": {
|
|
598
|
+
"type": "boolean"
|
|
599
|
+
},
|
|
600
|
+
"file": {
|
|
601
|
+
"type": "string"
|
|
602
|
+
},
|
|
603
|
+
"author": {
|
|
604
|
+
"type": "object",
|
|
605
|
+
"properties": {
|
|
606
|
+
"name": {
|
|
607
|
+
"type": "string"
|
|
608
|
+
},
|
|
609
|
+
"email": {
|
|
610
|
+
"type": "string"
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
"required": [],
|
|
614
|
+
"additionalProperties": false
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
"required": [],
|
|
618
|
+
"additionalProperties": false
|
|
619
|
+
},
|
|
594
620
|
"projects": {
|
|
595
621
|
"type": "array",
|
|
596
622
|
"items": {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"$schema": {
|
|
5
|
+
"type": "string"
|
|
6
|
+
},
|
|
7
|
+
"x18n": {
|
|
8
|
+
"const": 1
|
|
9
|
+
},
|
|
10
|
+
"threads": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"propertyNames": {
|
|
13
|
+
"type": "string"
|
|
14
|
+
},
|
|
15
|
+
"additionalProperties": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"propertyNames": {
|
|
18
|
+
"type": "string"
|
|
19
|
+
},
|
|
20
|
+
"additionalProperties": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"items": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"properties": {
|
|
25
|
+
"id": {
|
|
26
|
+
"type": "string"
|
|
27
|
+
},
|
|
28
|
+
"type": {
|
|
29
|
+
"enum": [
|
|
30
|
+
"comment",
|
|
31
|
+
"approve",
|
|
32
|
+
"request-change",
|
|
33
|
+
"suggestion"
|
|
34
|
+
],
|
|
35
|
+
"type": "string"
|
|
36
|
+
},
|
|
37
|
+
"author": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"name": {
|
|
41
|
+
"type": "string"
|
|
42
|
+
},
|
|
43
|
+
"email": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"required": [],
|
|
48
|
+
"additionalProperties": false
|
|
49
|
+
},
|
|
50
|
+
"time": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
},
|
|
53
|
+
"text": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
},
|
|
56
|
+
"suggestion": {
|
|
57
|
+
"type": "string"
|
|
58
|
+
},
|
|
59
|
+
"value": {
|
|
60
|
+
"type": "string"
|
|
61
|
+
},
|
|
62
|
+
"resolved": {
|
|
63
|
+
"type": "boolean"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"required": [
|
|
67
|
+
"id",
|
|
68
|
+
"type",
|
|
69
|
+
"time"
|
|
70
|
+
],
|
|
71
|
+
"additionalProperties": false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"sources": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"propertyNames": {
|
|
79
|
+
"type": "string"
|
|
80
|
+
},
|
|
81
|
+
"additionalProperties": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"propertyNames": {
|
|
84
|
+
"type": "string"
|
|
85
|
+
},
|
|
86
|
+
"additionalProperties": {
|
|
87
|
+
"type": "string"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"required": [
|
|
93
|
+
"x18n"
|
|
94
|
+
],
|
|
95
|
+
"additionalProperties": false,
|
|
96
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
97
|
+
"title": "x18n reviews"
|
|
98
|
+
}
|