@x18n/core 0.1.7 → 0.1.9
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 +77 -7
- package/dist/index.mjs +241 -62
- package/package.json +1 -1
- package/schema/config.json +15 -0
package/dist/index.d.mts
CHANGED
|
@@ -336,6 +336,21 @@ interface LocaleFinder {
|
|
|
336
336
|
readonly namespace: string | undefined;
|
|
337
337
|
readonly model: Resource | undefined;
|
|
338
338
|
}, ctx: FinderContext) => string | undefined) | undefined;
|
|
339
|
+
/**
|
|
340
|
+
* A file of its own for one key in a locale, for layouts with one file per message. A new message of the key goes
|
|
341
|
+
* there, created with `format` (default: by extension) when it does not exist; a rename moves the message to the
|
|
342
|
+
* file of the new key; a delete that leaves the file empty deletes it.
|
|
343
|
+
*/
|
|
344
|
+
readonly resolveKey?: ((target: {
|
|
345
|
+
readonly key: KeyString;
|
|
346
|
+
readonly locale: LocaleCode;
|
|
347
|
+
}, ctx: FinderContext) => KeyFile | undefined) | undefined;
|
|
348
|
+
}
|
|
349
|
+
/** The file of one key, from a locale finder's `resolveKey`. */
|
|
350
|
+
interface KeyFile {
|
|
351
|
+
readonly path: string;
|
|
352
|
+
/** Format id of a new file. Default: the format of its extension. */
|
|
353
|
+
readonly format?: string | undefined;
|
|
339
354
|
}
|
|
340
355
|
interface FormatContext {
|
|
341
356
|
readonly path: string;
|
|
@@ -349,6 +364,10 @@ interface ParseResult {
|
|
|
349
364
|
readonly ranges?: ReadonlyMap<string, NodeRange> | undefined;
|
|
350
365
|
/** What the file says about a message for translators (a note or comment next to it), keyed by `pathId(storedPath)`. */
|
|
351
366
|
readonly descriptions?: ReadonlyMap<string, string> | undefined;
|
|
367
|
+
/** The translation state the file records for a message (XLIFF `state`, PO `fuzzy`), keyed by `pathId(storedPath)`. */
|
|
368
|
+
readonly states?: ReadonlyMap<string, MessageState> | undefined;
|
|
369
|
+
/** Display labels of keys that do not read well, such as hashes, keyed by `pathId(storedPath)`. */
|
|
370
|
+
readonly labels?: ReadonlyMap<string, string> | undefined;
|
|
352
371
|
}
|
|
353
372
|
type MessageOp = {
|
|
354
373
|
readonly type: "set";
|
|
@@ -480,6 +499,10 @@ interface LocaleCoverage {
|
|
|
480
499
|
readonly empty: readonly KeyString[];
|
|
481
500
|
/** Translated keys whose message last changed before the source message. */
|
|
482
501
|
readonly outdated: readonly KeyString[];
|
|
502
|
+
/** Translated keys that the format marks `needs-review`. */
|
|
503
|
+
readonly needsReview: readonly KeyString[];
|
|
504
|
+
/** Translated keys that the format marks `reviewed`. */
|
|
505
|
+
readonly reviewed: readonly KeyString[];
|
|
483
506
|
}
|
|
484
507
|
/** When a message last changed, as a history provider knows it. */
|
|
485
508
|
interface MessageChange {
|
|
@@ -627,6 +650,29 @@ interface ResourceTransform {
|
|
|
627
650
|
/** Descriptions that the stored tree holds beside the messages, such as `{ "name": { "message": "...", "description": "..." } }`, keyed by `pathId(storedPath)`. */
|
|
628
651
|
readonly describe?: ((tree: MessageTree, ctx: ResourceTransformContext) => ReadonlyMap<string, string>) | undefined;
|
|
629
652
|
}
|
|
653
|
+
/** A review edit of a plan, after it is applied to the review data. */
|
|
654
|
+
interface ReviewEdit {
|
|
655
|
+
/** The review type of a new comment, or the op that resolved or applied one. */
|
|
656
|
+
readonly type: ReviewType | "resolve-review" | "apply-suggestion";
|
|
657
|
+
readonly key: KeyString;
|
|
658
|
+
readonly locale: LocaleCode;
|
|
659
|
+
/** The new comment, or the comment that was resolved or applied. */
|
|
660
|
+
readonly comment: ReviewComment;
|
|
661
|
+
/** The thread of the message after the edit, oldest first. */
|
|
662
|
+
readonly comments: readonly ReviewComment[];
|
|
663
|
+
}
|
|
664
|
+
interface ReviewEditContext {
|
|
665
|
+
/** A message before the plan. */
|
|
666
|
+
readonly message: (key: KeyString, locale: LocaleCode) => Message | undefined;
|
|
667
|
+
readonly resource: (id: ResourceId) => Resource | undefined;
|
|
668
|
+
/** The text of a project file as the plan leaves it so far, including earlier writes of `reviewEdit` hooks. */
|
|
669
|
+
readonly text: (path: string) => string | undefined;
|
|
670
|
+
}
|
|
671
|
+
/** The full new content of a file, which core puts into the plan. */
|
|
672
|
+
interface FileWrite {
|
|
673
|
+
readonly path: string;
|
|
674
|
+
readonly content: string;
|
|
675
|
+
}
|
|
630
676
|
interface PromptDraft {
|
|
631
677
|
readonly rules: string[];
|
|
632
678
|
readonly notes: string[];
|
|
@@ -665,6 +711,8 @@ interface Plugin {
|
|
|
665
711
|
readonly derivedKeys?: readonly string[] | undefined;
|
|
666
712
|
readonly transformResource?: ResourceTransform | undefined;
|
|
667
713
|
readonly translatePrompt?: ((draft: PromptDraft, ctx: PromptContext) => void) | undefined;
|
|
714
|
+
/** Called for each review edit of a plan, in op order. The writes it returns go into the same plan. */
|
|
715
|
+
readonly reviewEdit?: ((edit: ReviewEdit, ctx: ReviewEditContext) => readonly FileWrite[] | undefined) | undefined;
|
|
668
716
|
/** Display data for a locale code. The first plugin that returns a value wins. */
|
|
669
717
|
readonly localeInfo?: ((code: LocaleCode) => Partial<LocaleInfo> | undefined) | undefined;
|
|
670
718
|
}
|
|
@@ -775,11 +823,15 @@ interface GlossaryEntry {
|
|
|
775
823
|
readonly translations?: {
|
|
776
824
|
readonly [locale: string]: string;
|
|
777
825
|
} | undefined;
|
|
826
|
+
/** Other forms per locale, matched at the start of a word: inflections and compound stems of the translation, or of the term under the source locale. */
|
|
827
|
+
readonly forms?: {
|
|
828
|
+
readonly [locale: string]: readonly string[];
|
|
829
|
+
} | undefined;
|
|
778
830
|
readonly note?: string | undefined;
|
|
779
831
|
readonly doNotTranslate?: boolean | undefined;
|
|
780
832
|
}
|
|
781
833
|
interface ExtractTemplate {
|
|
782
|
-
/** Replacement code
|
|
834
|
+
/** Replacement code. `{key}` is the key, `{attr}` the attribute name, `{args}` the arguments of a template string, `{text}` the message of the hard string, for a default value, `{source}` the text as written in the file, and `{parts}` the message as the body of a template string. `{key}` and `{text}` are escaped for the quotes around them. */
|
|
783
835
|
readonly template: string;
|
|
784
836
|
/** Hard-string kinds the template applies to, for example `jsx-text`. */
|
|
785
837
|
readonly kinds?: readonly string[] | undefined;
|
|
@@ -813,6 +865,8 @@ interface ExtractOptions {
|
|
|
813
865
|
interface UsageOptions {
|
|
814
866
|
readonly include?: readonly string[] | undefined;
|
|
815
867
|
readonly exclude?: readonly string[] | undefined;
|
|
868
|
+
/** Skip the files that `.gitignore` files ignore. Default `true`. Set `false` to scan a checkout that git ignores, such as a clone of the sources in a translations repository. */
|
|
869
|
+
readonly gitignore?: boolean | undefined;
|
|
816
870
|
readonly calls?: readonly CallSpec[] | undefined;
|
|
817
871
|
readonly regex?: readonly RegexScannerSpec[] | undefined;
|
|
818
872
|
/** Callee names never counted as translate calls. */
|
|
@@ -888,7 +942,7 @@ interface SerializableConfig {
|
|
|
888
942
|
} | undefined;
|
|
889
943
|
/** Turn every warning into an error. */
|
|
890
944
|
readonly strict?: boolean | undefined;
|
|
891
|
-
/** Keys (globs) excluded from all checks. */
|
|
945
|
+
/** Keys (globs) excluded from all checks. `x18n.config.ts` also takes regular expressions. */
|
|
892
946
|
readonly ignoreKeys?: readonly string[] | undefined;
|
|
893
947
|
readonly glossary?: readonly GlossaryEntry[] | undefined;
|
|
894
948
|
/** 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`). */
|
|
@@ -911,9 +965,13 @@ interface HistorySettings extends HistoryOptions {
|
|
|
911
965
|
}
|
|
912
966
|
type PresetOption = Preset | PluginSpecifier | false | undefined;
|
|
913
967
|
type PluginOption = Plugin | Preset | PluginSpecifier | false | undefined;
|
|
968
|
+
/** A key glob (`*` inside a segment, `**` across segments), or a regular expression tested against the whole key. */
|
|
969
|
+
type KeyPattern = string | RegExp;
|
|
914
970
|
/** The config accepted by `defineConfig`: the serializable part plus plugin objects. */
|
|
915
|
-
interface UserConfig extends Omit<SerializableConfig, "presets" | "plugins" | "checks" | "history"> {
|
|
971
|
+
interface UserConfig extends Omit<SerializableConfig, "presets" | "plugins" | "checks" | "history" | "ignoreKeys"> {
|
|
916
972
|
readonly presets?: "auto" | readonly PresetOption[] | undefined;
|
|
973
|
+
/** Keys excluded from all checks: globs, or regular expressions tested against the whole key (`ns:a.b` when namespaced). */
|
|
974
|
+
readonly ignoreKeys?: readonly KeyPattern[] | undefined;
|
|
917
975
|
/** History providers for stale translations: `false` turns them off; provider options such as `git` sit under their key; `providers` adds providers asked first. */
|
|
918
976
|
readonly history?: boolean | (HistoryOptions & {
|
|
919
977
|
readonly providers?: readonly HistoryProvider[] | undefined;
|
|
@@ -952,6 +1010,7 @@ interface ResolvedConfig {
|
|
|
952
1010
|
readonly usage: {
|
|
953
1011
|
readonly include: readonly string[];
|
|
954
1012
|
readonly exclude: readonly string[];
|
|
1013
|
+
readonly gitignore: boolean;
|
|
955
1014
|
readonly calls: readonly CallSpec[];
|
|
956
1015
|
readonly regex: readonly RegexScannerSpec[];
|
|
957
1016
|
readonly ignoreCallees: readonly string[];
|
|
@@ -968,7 +1027,7 @@ interface ResolvedConfig {
|
|
|
968
1027
|
readonly [checkId: string]: CheckSetting;
|
|
969
1028
|
};
|
|
970
1029
|
readonly strict: boolean;
|
|
971
|
-
readonly ignoreKeys: readonly
|
|
1030
|
+
readonly ignoreKeys: readonly KeyPattern[];
|
|
972
1031
|
readonly glossary: readonly GlossaryEntry[];
|
|
973
1032
|
readonly history: false | HistorySettings;
|
|
974
1033
|
readonly review: {
|
|
@@ -1051,6 +1110,8 @@ interface Resource {
|
|
|
1051
1110
|
/** Key separator of the resource when it differs from the config's `keySeparator` (from its resource spec). */
|
|
1052
1111
|
readonly keySeparator?: KeySeparator | undefined;
|
|
1053
1112
|
}
|
|
1113
|
+
/** How far a translation got, as in XLIFF: a value can exist before anyone reviews it. */
|
|
1114
|
+
type MessageState = "translated" | "needs-review" | "reviewed";
|
|
1054
1115
|
/** One stored value of one key in one locale. */
|
|
1055
1116
|
interface Message {
|
|
1056
1117
|
readonly key: KeyString;
|
|
@@ -1063,6 +1124,10 @@ interface Message {
|
|
|
1063
1124
|
readonly range: NodeRange | undefined;
|
|
1064
1125
|
/** What the resource says about the message for translators, such as a note or a comment beside it. */
|
|
1065
1126
|
readonly description?: string | undefined;
|
|
1127
|
+
/** The translation state the resource records, such as XLIFF `state` or PO `fuzzy`. */
|
|
1128
|
+
readonly state?: MessageState | undefined;
|
|
1129
|
+
/** A display label of the key from the resource, for keys that do not read well, such as hashes. */
|
|
1130
|
+
readonly label?: string | undefined;
|
|
1066
1131
|
}
|
|
1067
1132
|
type UsageKind = "call" | "selector" | "accessor" | "constant" | "marker";
|
|
1068
1133
|
interface Usage {
|
|
@@ -1247,8 +1312,11 @@ export declare function replaceKeyPrefix(key: KeyString, from: KeyString, to: Ke
|
|
|
1247
1312
|
* `{a,b}` alternates, `[...]` is a character class.
|
|
1248
1313
|
*/
|
|
1249
1314
|
export declare function globToRegExp(glob: string, separator?: string): RegExp;
|
|
1250
|
-
/**
|
|
1251
|
-
|
|
1315
|
+
/**
|
|
1316
|
+
* Matcher over key patterns. In a glob `*` stays inside a segment and `**` spans segments; with literal keys
|
|
1317
|
+
* (`separator: false`) `*` spans the whole key. A regular expression is tested against the whole key as is.
|
|
1318
|
+
*/
|
|
1319
|
+
export declare function keyMatcher(patterns: readonly KeyPattern[], separator?: KeySeparator): (key: KeyString) => boolean;
|
|
1252
1320
|
/** Matcher over posix file paths. */
|
|
1253
1321
|
export declare function pathMatcher(globs: readonly string[]): (path: string) => boolean;
|
|
1254
1322
|
//#endregion
|
|
@@ -1523,6 +1591,8 @@ interface KeyInfo {
|
|
|
1523
1591
|
readonly key: KeyString;
|
|
1524
1592
|
readonly namespace: string | undefined;
|
|
1525
1593
|
readonly path: readonly string[];
|
|
1594
|
+
/** Display label of the key: the first `label` of its messages, source locale first. */
|
|
1595
|
+
readonly label: string | undefined;
|
|
1526
1596
|
/** One message per locale that has the key, source locale first. */
|
|
1527
1597
|
readonly messages: readonly Message[];
|
|
1528
1598
|
readonly missing: readonly LocaleCode[];
|
|
@@ -1876,4 +1946,4 @@ export declare function rangeContains(range: TextRange, offset: number): boolean
|
|
|
1876
1946
|
/** Fast non-cryptographic string hash (cyrb53), as hex. */
|
|
1877
1947
|
export declare function hashText(text: string): string;
|
|
1878
1948
|
//#endregion
|
|
1879
|
-
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, KeyId, KeyInfo, KeyQuery, KeySeparator, 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, 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 };
|
|
1949
|
+
export type { Analysis, Answers, ApplyResult, ArgSource, BaselineSummary, Builtins, CacheStore, CallSpec, CapabilityConflict, Check, CheckContext, CheckIds, CheckLevel, CheckSetting, ConfigContext, ConfigDraft, ConfigOrigin, CreateProjectOptions, Decision, DecisionOption, DetectContext, Diagnostic, DiagnosticInput, Disposable, EditOp, Envelope, ErrorCode, ErrorInfo, ExtractItem, ExtractOptions, ExtractTemplate, Extractor, FileChange, FileEvent, FileScan, FileWrite, FinderContext, Fix, Format, FormatContext, FormatEntry, GlobOptions, GlossaryEntry, HardString, HistoryConfigs, HistoryContext, HistoryOptions, HistoryProvider, HistorySettings, Host, JsonObject, JsonPrimitive, JsonValue, KeptUnusedKey, KeyContext, KeyFile, KeyId, KeyInfo, KeyPattern, KeyQuery, KeySeparator, KeyString, KeyTarget, LazyFormat, LazyScanner, LibraryOptions, LineIndex, LinePosition, LocaleCode, LocaleCoverage, LocaleFinder, LocaleInfo, Location, Logger, ManifestReader, MemoryHost, Message, MessageChange, MessageHistory, MessageLeaf, MessageOp, MessageState, MessageTree, NamespaceFromPath, NamespaceOptions, NodeRange, OverlayHost, ParamsSource, ParseResult, Placeholder, PlaceholderComparer, PlaceholderProblem, PlaceholderSyntax, Plan, Plugin, PluginConfigs, PluginFile, PluginLoader, PluginOption, PluginRegistry, PluginSpecifier, PluralForm, Preset, PresetDetection, PresetEntry, PresetExplanation, PresetFactory, PresetLoader, PresetOption, Project, ProjectChange, ProjectExplanation, PromptContext, PromptData, PromptDraft, PromptIssue, PromptItem, PromptOptions, RegexScannerSpec, RelatedValue, ReportInput, ReportSummary, Reporter, ReporterContext, ReporterLoader, ResolveConfigOptions, ResolvedConfig, Resource, ResourceBlock, ResourceCandidate, ResourceId, ResourceNode, ResourceSpec, ResourceTransform, ResourceTransformContext, ReviewAuthor, ReviewComment, ReviewData, ReviewEdit, ReviewEditContext, ReviewFile, ReviewFilter, ReviewOptions, ReviewState, ReviewStore, ReviewThread, ReviewType, ScanContext, ScanResult, Scanner, ScannerEntry, SchemaIssue, Scope, SearchMatch, SearchOptions, SerializableConfig, Setup, Severity, SourceFile, TextEdit, TextRange, ThreadComment, TreeLeaf, UnusedDeletion, UnusedKeyDeletion, UnusedQuery, Usage, UsageCandidate, UsageContext, UsageKind, UsageMatch, UsageOptions, UsageQuery, UserConfig, WireDiagnostic, WireRange, WriteOptions, X18nError };
|
package/dist/index.mjs
CHANGED
|
@@ -379,10 +379,13 @@ function globToRegExp(glob, separator = "/") {
|
|
|
379
379
|
}
|
|
380
380
|
return new RegExp(`^${out}$`);
|
|
381
381
|
}
|
|
382
|
-
/**
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Matcher over key patterns. In a glob `*` stays inside a segment and `**` spans segments; with literal keys
|
|
384
|
+
* (`separator: false`) `*` spans the whole key. A regular expression is tested against the whole key as is.
|
|
385
|
+
*/
|
|
386
|
+
function keyMatcher(patterns, separator = ".") {
|
|
387
|
+
if (patterns.length === 0) return () => false;
|
|
388
|
+
const regexes = patterns.map((pattern) => pattern instanceof RegExp ? new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, "")) : globToRegExp(pattern, separator === false ? "\0" : separator));
|
|
386
389
|
return (key) => regexes.some((re) => re.test(key));
|
|
387
390
|
}
|
|
388
391
|
/** Matcher over posix file paths. */
|
|
@@ -621,11 +624,15 @@ function deleteIn(tree, path) {
|
|
|
621
624
|
delete parents[i - 1][path[i - 1]];
|
|
622
625
|
}
|
|
623
626
|
}
|
|
627
|
+
/** Applies ops to the tree itself. Parent objects left empty are removed. */
|
|
628
|
+
function applyMessageOpsInPlace(tree, ops) {
|
|
629
|
+
for (const op of ops) if (op.type === "set") setIn(tree, op.path, op.value);
|
|
630
|
+
else deleteIn(tree, op.path);
|
|
631
|
+
}
|
|
624
632
|
/** Applies ops to a copy of the tree. Parent objects left empty are removed. */
|
|
625
633
|
function applyMessageOps(tree, ops) {
|
|
626
634
|
const out = cloneTree(tree);
|
|
627
|
-
|
|
628
|
-
else deleteIn(out, op.path);
|
|
635
|
+
applyMessageOpsInPlace(out, ops);
|
|
629
636
|
return out;
|
|
630
637
|
}
|
|
631
638
|
/** Sorts keys at every level. */
|
|
@@ -882,6 +889,7 @@ const extractOptions = v.strictObject({
|
|
|
882
889
|
const glossaryEntry = v.strictObject({
|
|
883
890
|
term: v.string(),
|
|
884
891
|
translations: v.optional(v.record(v.string(), v.string())),
|
|
892
|
+
forms: v.optional(v.record(v.string(), v.array(v.string()))),
|
|
885
893
|
note: v.optional(v.string()),
|
|
886
894
|
doNotTranslate: v.optional(v.boolean())
|
|
887
895
|
});
|
|
@@ -924,6 +932,7 @@ const ConfigSchema = v.strictObject({
|
|
|
924
932
|
usage: v.optional(v.strictObject({
|
|
925
933
|
include: v.optional(strings),
|
|
926
934
|
exclude: v.optional(strings),
|
|
935
|
+
gitignore: v.optional(v.boolean()),
|
|
927
936
|
calls: v.optional(v.array(callSpec)),
|
|
928
937
|
regex: v.optional(v.array(regexSpec)),
|
|
929
938
|
ignoreCallees: v.optional(strings),
|
|
@@ -1370,12 +1379,19 @@ function computeCoverage(state, options, sourceKeys) {
|
|
|
1370
1379
|
const missing = [];
|
|
1371
1380
|
const empty = [];
|
|
1372
1381
|
const outdated = [];
|
|
1382
|
+
const needsReview = [];
|
|
1383
|
+
const reviewed = [];
|
|
1373
1384
|
for (const key of reference) {
|
|
1374
|
-
const
|
|
1385
|
+
const message = messages.get(key)?.get(locale)?.[0];
|
|
1386
|
+
const value = message?.value;
|
|
1375
1387
|
if (value === void 0 || value === "") {
|
|
1376
1388
|
if (inherited(key)) continue;
|
|
1377
1389
|
(value === void 0 ? missing : empty).push(key);
|
|
1378
|
-
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
if (locale !== sourceLocale && isOutdated(state.history(key, sourceLocale), state.history(key, locale)) && !state.fresh(key, locale)) outdated.push(key);
|
|
1393
|
+
if (message?.state === "needs-review") needsReview.push(key);
|
|
1394
|
+
else if (message?.state === "reviewed") reviewed.push(key);
|
|
1379
1395
|
}
|
|
1380
1396
|
return {
|
|
1381
1397
|
locale,
|
|
@@ -1383,7 +1399,9 @@ function computeCoverage(state, options, sourceKeys) {
|
|
|
1383
1399
|
translated: reference.length - missing.length - empty.length,
|
|
1384
1400
|
missing,
|
|
1385
1401
|
empty,
|
|
1386
|
-
outdated
|
|
1402
|
+
outdated,
|
|
1403
|
+
needsReview,
|
|
1404
|
+
reviewed
|
|
1387
1405
|
};
|
|
1388
1406
|
});
|
|
1389
1407
|
}
|
|
@@ -1725,6 +1743,10 @@ function nodeAt(tree, path) {
|
|
|
1725
1743
|
}
|
|
1726
1744
|
return node;
|
|
1727
1745
|
}
|
|
1746
|
+
function leafAt(tree, path) {
|
|
1747
|
+
const node = nodeAt(tree, path);
|
|
1748
|
+
return isMessageLeaf(node) ? node : void 0;
|
|
1749
|
+
}
|
|
1728
1750
|
/** True for flat files (`{ "a.b": "x" }`), false for nested ones, undefined when it cannot tell. */
|
|
1729
1751
|
function isFlatTree(tree, separator) {
|
|
1730
1752
|
const entries = Object.entries(tree);
|
|
@@ -1771,6 +1793,10 @@ var PlanBuilder = class {
|
|
|
1771
1793
|
answers;
|
|
1772
1794
|
work = /* @__PURE__ */ new Map();
|
|
1773
1795
|
created = /* @__PURE__ */ new Map();
|
|
1796
|
+
/** New files that a finder's `resolveKey` gave one key. */
|
|
1797
|
+
keyFiles = /* @__PURE__ */ new Map();
|
|
1798
|
+
/** Resources that are the file of a key deleted from them: removed once empty. */
|
|
1799
|
+
keyFilesLeft = /* @__PURE__ */ new Set();
|
|
1774
1800
|
sourceEdits = /* @__PURE__ */ new Map();
|
|
1775
1801
|
sourceImports = /* @__PURE__ */ new Map();
|
|
1776
1802
|
decisions = [];
|
|
@@ -1780,6 +1806,7 @@ var PlanBuilder = class {
|
|
|
1780
1806
|
config;
|
|
1781
1807
|
/** Source values set by this plan, for the source records of targets set after them. */
|
|
1782
1808
|
plannedSource = /* @__PURE__ */ new Map();
|
|
1809
|
+
reviewEdits = [];
|
|
1783
1810
|
draft;
|
|
1784
1811
|
constructor(options, state, answers) {
|
|
1785
1812
|
this.options = options;
|
|
@@ -1807,7 +1834,7 @@ var PlanBuilder = class {
|
|
|
1807
1834
|
if (!entry) {
|
|
1808
1835
|
entry = {
|
|
1809
1836
|
target,
|
|
1810
|
-
tree: target.state?.rawTree ?? {},
|
|
1837
|
+
tree: cloneTree(target.state?.rawTree ?? {}),
|
|
1811
1838
|
ops: [],
|
|
1812
1839
|
keyRenames: [],
|
|
1813
1840
|
formatOnly: false
|
|
@@ -1851,7 +1878,8 @@ var PlanBuilder = class {
|
|
|
1851
1878
|
const stored = this.options.toStored(entry.target, relative);
|
|
1852
1879
|
const separator = this.separatorOf(entry.target);
|
|
1853
1880
|
if (separator === false) return stored;
|
|
1854
|
-
|
|
1881
|
+
if (stored.length === relative.length) entry.flat ??= isFlatTree(entry.tree, separator);
|
|
1882
|
+
return stored.length === relative.length && (entry.flat ?? this.config.keyStyle === "flat") ? [stored.join(separator)] : stored;
|
|
1855
1883
|
}
|
|
1856
1884
|
createTarget(key, locale) {
|
|
1857
1885
|
const { resources, sourceLocale, namespaced } = this.state;
|
|
@@ -1898,8 +1926,40 @@ var PlanBuilder = class {
|
|
|
1898
1926
|
this.created.set(path, target);
|
|
1899
1927
|
return target;
|
|
1900
1928
|
}
|
|
1901
|
-
/**
|
|
1929
|
+
/** The file a locale finder gives the key, created when it does not exist. */
|
|
1930
|
+
keyTarget(key, locale) {
|
|
1931
|
+
const file = this.options.resolveKey(key, locale);
|
|
1932
|
+
if (!file) return void 0;
|
|
1933
|
+
const existing = this.state.resources.find((s) => s.resource.path === file.path && !s.resource.block);
|
|
1934
|
+
if (existing) {
|
|
1935
|
+
if (existing.resource.locale !== locale || this.relativePath(key, existing.resource) === void 0) throw errors.X18N4005({
|
|
1936
|
+
key,
|
|
1937
|
+
locale
|
|
1938
|
+
});
|
|
1939
|
+
return this.targetOf(existing);
|
|
1940
|
+
}
|
|
1941
|
+
const pending = this.keyFiles.get(file.path);
|
|
1942
|
+
if (pending) return pending;
|
|
1943
|
+
const format = file.format === void 0 ? this.options.registry.formatFor(file.path) : this.options.registry.formatById(file.format);
|
|
1944
|
+
if (!format?.serialize) throw errors.X18N4005({
|
|
1945
|
+
key,
|
|
1946
|
+
locale
|
|
1947
|
+
});
|
|
1948
|
+
const target = {
|
|
1949
|
+
id: file.path,
|
|
1950
|
+
path: file.path,
|
|
1951
|
+
locale,
|
|
1952
|
+
namespace: this.state.namespaced ? this.keyId(key).namespace ?? this.config.namespace.default : void 0,
|
|
1953
|
+
format,
|
|
1954
|
+
state: void 0
|
|
1955
|
+
};
|
|
1956
|
+
this.keyFiles.set(file.path, target);
|
|
1957
|
+
return target;
|
|
1958
|
+
}
|
|
1959
|
+
/** Placement: the key's own file, then the namespace's resource, then the longest shared prefix, then the first; a tie is a decision. */
|
|
1902
1960
|
pickTarget(key, locale) {
|
|
1961
|
+
const own = this.keyTarget(key, locale);
|
|
1962
|
+
if (own) return own;
|
|
1903
1963
|
let inLocale = this.state.resources.filter((s) => s.resource.locale === locale && this.relativePath(key, s.resource) !== void 0);
|
|
1904
1964
|
const source = this.options.message(key, this.state.sourceLocale);
|
|
1905
1965
|
const sourceNamespace = source && this.stateById.get(source.resource)?.resource.namespace;
|
|
@@ -1932,18 +1992,30 @@ var PlanBuilder = class {
|
|
|
1932
1992
|
/** ---- Message ops ---- */
|
|
1933
1993
|
pushOp(entry, op) {
|
|
1934
1994
|
entry.ops.push(op);
|
|
1935
|
-
|
|
1995
|
+
this.applyOps(entry, [op]);
|
|
1996
|
+
}
|
|
1997
|
+
/** 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. */
|
|
1998
|
+
applyOps(entry, ops) {
|
|
1999
|
+
for (const op of ops) {
|
|
2000
|
+
const top = op.path[0];
|
|
2001
|
+
const before = entry.tree[top];
|
|
2002
|
+
applyMessageOpsInPlace(entry.tree, [op]);
|
|
2003
|
+
const after = entry.tree[top];
|
|
2004
|
+
if ((before !== void 0 && !isMessageLeaf(before)) !== (after !== void 0 && !isMessageLeaf(after)) || before !== void 0 && after === void 0) entry.flat = void 0;
|
|
2005
|
+
}
|
|
1936
2006
|
}
|
|
1937
2007
|
setValue(key, locale, value) {
|
|
1938
2008
|
this.touched.add(key);
|
|
1939
2009
|
const existing = this.options.message(key, locale);
|
|
1940
2010
|
if (existing) {
|
|
1941
|
-
this.
|
|
2011
|
+
const entry = this.workFor(this.targetOf(this.writable(existing.resource)));
|
|
2012
|
+
const before = leafAt(entry.tree, existing.storedPath);
|
|
2013
|
+
this.pushOp(entry, {
|
|
1942
2014
|
type: "set",
|
|
1943
2015
|
path: existing.storedPath,
|
|
1944
2016
|
value
|
|
1945
2017
|
});
|
|
1946
|
-
return;
|
|
2018
|
+
return before;
|
|
1947
2019
|
}
|
|
1948
2020
|
const target = this.pickTarget(key, locale);
|
|
1949
2021
|
if (target.state) this.writable(target.state.resource.id);
|
|
@@ -1956,9 +2028,12 @@ var PlanBuilder = class {
|
|
|
1956
2028
|
path: stored,
|
|
1957
2029
|
value
|
|
1958
2030
|
});
|
|
2031
|
+
return isMessageLeaf(occupied) ? occupied : void 0;
|
|
1959
2032
|
}
|
|
1960
2033
|
deleteMessage(m) {
|
|
1961
|
-
const
|
|
2034
|
+
const s = this.writable(m.resource);
|
|
2035
|
+
if (!s.resource.block && this.options.resolveKey(m.key, m.locale)?.path === s.resource.path) this.keyFilesLeft.add(s.resource.id);
|
|
2036
|
+
const entry = this.workFor(this.targetOf(s));
|
|
1962
2037
|
let path = [...m.storedPath];
|
|
1963
2038
|
while (path.length > 1) {
|
|
1964
2039
|
const parent = nodeAt(entry.tree, path.slice(0, -1));
|
|
@@ -1997,7 +2072,7 @@ var PlanBuilder = class {
|
|
|
1997
2072
|
text: last
|
|
1998
2073
|
});
|
|
1999
2074
|
const leaves = [...walkLeaves(node, stored)];
|
|
2000
|
-
|
|
2075
|
+
this.applyOps(entry, [...leaves.map((leaf) => ({
|
|
2001
2076
|
type: "delete",
|
|
2002
2077
|
path: leaf.path
|
|
2003
2078
|
})), ...leaves.map((leaf) => ({
|
|
@@ -2012,44 +2087,54 @@ var PlanBuilder = class {
|
|
|
2012
2087
|
moveKey(from, to, renamed = /* @__PURE__ */ new Set()) {
|
|
2013
2088
|
this.touched.add(from);
|
|
2014
2089
|
this.touched.add(to);
|
|
2015
|
-
for (const
|
|
2016
|
-
const m = list[0];
|
|
2090
|
+
for (const list of this.options.messages(from)?.values() ?? []) for (const m of [...list].reverse()) {
|
|
2017
2091
|
if (renamed.has(m.resource)) continue;
|
|
2018
|
-
const
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
this.
|
|
2022
|
-
this.setValue(to, locale, m.value);
|
|
2023
|
-
continue;
|
|
2024
|
-
}
|
|
2025
|
-
const entry = this.workFor(this.targetOf(s));
|
|
2026
|
-
const separator = this.separatorOf(s.resource);
|
|
2027
|
-
const flat = separator !== false && m.storedPath.length === 1 && m.storedPath[0].includes(separator);
|
|
2028
|
-
const next = flat ? [relative.join(separator)] : this.options.toStored(s.resource, relative);
|
|
2029
|
-
const sameParent = next.length === m.storedPath.length && next.slice(0, -1).every((segment, i) => segment === m.storedPath[i]);
|
|
2030
|
-
const last = next.at(-1);
|
|
2031
|
-
if (sameParent && m.range && (flat ? safeFlat : safeSegment).test(last)) {
|
|
2032
|
-
entry.keyRenames.push({
|
|
2033
|
-
range: m.range.key,
|
|
2034
|
-
text: last
|
|
2035
|
-
});
|
|
2036
|
-
entry.tree = applyMessageOps(entry.tree, [{
|
|
2037
|
-
type: "delete",
|
|
2038
|
-
path: m.storedPath
|
|
2039
|
-
}, {
|
|
2040
|
-
type: "set",
|
|
2041
|
-
path: next,
|
|
2042
|
-
value: m.value
|
|
2043
|
-
}]);
|
|
2044
|
-
continue;
|
|
2092
|
+
const replaced = this.moveMessage(m, to);
|
|
2093
|
+
if (replaced !== void 0 && replaced !== m.value) {
|
|
2094
|
+
const path = this.stateById.get(m.resource)?.resource.path ?? m.resource;
|
|
2095
|
+
this.warnings.push(`Copies of "${from}" in ${m.locale} have different values; "${to}" keeps the one from ${path}.`);
|
|
2045
2096
|
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
moveMessage(m, to) {
|
|
2100
|
+
const s = this.writable(m.resource);
|
|
2101
|
+
const relative = this.relativePath(to, s.resource);
|
|
2102
|
+
const own = this.options.resolveKey(to, m.locale);
|
|
2103
|
+
if (!relative || own && own.path !== s.resource.path) {
|
|
2046
2104
|
this.deleteMessage(m);
|
|
2047
|
-
this.
|
|
2105
|
+
return this.setValue(to, m.locale, m.value);
|
|
2106
|
+
}
|
|
2107
|
+
const entry = this.workFor(this.targetOf(s));
|
|
2108
|
+
const separator = this.separatorOf(s.resource);
|
|
2109
|
+
const flat = separator !== false && m.storedPath.length === 1 && m.storedPath[0].includes(separator);
|
|
2110
|
+
const next = flat ? [relative.join(separator)] : this.options.toStored(s.resource, relative);
|
|
2111
|
+
const sameParent = next.length === m.storedPath.length && next.slice(0, -1).every((segment, i) => segment === m.storedPath[i]);
|
|
2112
|
+
const last = next.at(-1);
|
|
2113
|
+
if (sameParent && m.range && (flat ? safeFlat : safeSegment).test(last)) {
|
|
2114
|
+
const replaced = leafAt(entry.tree, next);
|
|
2115
|
+
entry.keyRenames.push({
|
|
2116
|
+
range: m.range.key,
|
|
2117
|
+
text: last
|
|
2118
|
+
});
|
|
2119
|
+
this.applyOps(entry, [{
|
|
2120
|
+
type: "delete",
|
|
2121
|
+
path: m.storedPath
|
|
2122
|
+
}, {
|
|
2048
2123
|
type: "set",
|
|
2049
|
-
path:
|
|
2124
|
+
path: next,
|
|
2050
2125
|
value: m.value
|
|
2051
|
-
});
|
|
2126
|
+
}]);
|
|
2127
|
+
return replaced;
|
|
2052
2128
|
}
|
|
2129
|
+
this.deleteMessage(m);
|
|
2130
|
+
const path = this.storedPathFor(entry, relative);
|
|
2131
|
+
const replaced = leafAt(entry.tree, path);
|
|
2132
|
+
this.pushOp(entry, {
|
|
2133
|
+
type: "set",
|
|
2134
|
+
path,
|
|
2135
|
+
value: m.value
|
|
2136
|
+
});
|
|
2137
|
+
return replaced;
|
|
2053
2138
|
}
|
|
2054
2139
|
/** The raw text a usage needs to refer to `next`, given its scope. */
|
|
2055
2140
|
rawFor(usage, next) {
|
|
@@ -2189,6 +2274,17 @@ var PlanBuilder = class {
|
|
|
2189
2274
|
...op.review === "suggestion" && { suggestion: op.suggestion },
|
|
2190
2275
|
...op.review === "approve" && current !== void 0 && { value: valueText(current) }
|
|
2191
2276
|
});
|
|
2277
|
+
this.reviewEdited(draft, op.review, op.id);
|
|
2278
|
+
}
|
|
2279
|
+
reviewEdited(draft, type, id) {
|
|
2280
|
+
const { key, locale, comment } = findReview(draft.data, id);
|
|
2281
|
+
this.reviewEdits.push({
|
|
2282
|
+
type,
|
|
2283
|
+
key,
|
|
2284
|
+
locale,
|
|
2285
|
+
comment,
|
|
2286
|
+
comments: draft.data.threads[key][locale]
|
|
2287
|
+
});
|
|
2192
2288
|
}
|
|
2193
2289
|
applySuggestion(id) {
|
|
2194
2290
|
const draft = this.requireReviews();
|
|
@@ -2199,6 +2295,7 @@ var PlanBuilder = class {
|
|
|
2199
2295
|
this.setValue(key, locale, comment.suggestion);
|
|
2200
2296
|
this.recordSource(key, locale, true);
|
|
2201
2297
|
draft.resolve(id);
|
|
2298
|
+
this.reviewEdited(draft, "apply-suggestion", id);
|
|
2202
2299
|
}
|
|
2203
2300
|
resolveReview(id) {
|
|
2204
2301
|
const draft = this.requireReviews();
|
|
@@ -2206,6 +2303,7 @@ var PlanBuilder = class {
|
|
|
2206
2303
|
if (!found) throw errors.X18N5004({ id });
|
|
2207
2304
|
this.touched.add(found.key);
|
|
2208
2305
|
draft.resolve(id);
|
|
2306
|
+
this.reviewEdited(draft, "resolve-review", id);
|
|
2209
2307
|
}
|
|
2210
2308
|
markFresh(op) {
|
|
2211
2309
|
this.requireReviews();
|
|
@@ -2326,6 +2424,14 @@ var PlanBuilder = class {
|
|
|
2326
2424
|
texts.set(target.path, void 0);
|
|
2327
2425
|
continue;
|
|
2328
2426
|
}
|
|
2427
|
+
if (this.keyFilesLeft.has(s.resource.id) && Object.keys(entry.tree).length === 0) {
|
|
2428
|
+
changes.push({
|
|
2429
|
+
type: "delete",
|
|
2430
|
+
path: target.path
|
|
2431
|
+
});
|
|
2432
|
+
texts.set(target.path, this.state.fileTexts.get(target.path));
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
2329
2435
|
const edits = diffText(s.content, this.nextContent(entry, s)).map((e) => ({
|
|
2330
2436
|
range: {
|
|
2331
2437
|
start: e.range.start + s.offset,
|
|
@@ -2357,6 +2463,7 @@ var PlanBuilder = class {
|
|
|
2357
2463
|
});
|
|
2358
2464
|
texts.set(path, text);
|
|
2359
2465
|
}
|
|
2466
|
+
this.reviewWrites(changes, texts);
|
|
2360
2467
|
const review = this.reviewChange();
|
|
2361
2468
|
if (review) {
|
|
2362
2469
|
changes.push(review);
|
|
@@ -2367,6 +2474,41 @@ var PlanBuilder = class {
|
|
|
2367
2474
|
texts
|
|
2368
2475
|
};
|
|
2369
2476
|
}
|
|
2477
|
+
/** Runs the `reviewEdit` hooks and puts their writes into the changes. */
|
|
2478
|
+
reviewWrites(changes, texts) {
|
|
2479
|
+
const hooks = this.options.registry.plugins.flatMap((plugin) => plugin.reviewEdit ? [plugin.reviewEdit] : []);
|
|
2480
|
+
if (hooks.length === 0 || this.reviewEdits.length === 0) return;
|
|
2481
|
+
const written = /* @__PURE__ */ new Map();
|
|
2482
|
+
const text = (path) => {
|
|
2483
|
+
if (written.has(path)) return written.get(path);
|
|
2484
|
+
const change = changes.find((c) => c.path === path);
|
|
2485
|
+
if (!change) return this.options.text(path);
|
|
2486
|
+
return change.type === "create" ? change.content : change.type === "edit" ? applyTextEdits(texts.get(path) ?? "", change.edits) : void 0;
|
|
2487
|
+
};
|
|
2488
|
+
const ctx = {
|
|
2489
|
+
message: this.options.message,
|
|
2490
|
+
resource: (id) => this.stateById.get(id)?.resource,
|
|
2491
|
+
text
|
|
2492
|
+
};
|
|
2493
|
+
for (const edit of this.reviewEdits) for (const hook of hooks) for (const write of hook(edit, ctx) ?? []) written.set(write.path, write.content);
|
|
2494
|
+
for (const [path, content] of written) {
|
|
2495
|
+
const index = changes.findIndex((c) => c.path === path);
|
|
2496
|
+
const before = index === -1 ? this.options.text(path) : texts.get(path);
|
|
2497
|
+
if (index === -1 && before === content) continue;
|
|
2498
|
+
const change = before === void 0 ? {
|
|
2499
|
+
type: "create",
|
|
2500
|
+
path,
|
|
2501
|
+
content
|
|
2502
|
+
} : {
|
|
2503
|
+
type: "edit",
|
|
2504
|
+
path,
|
|
2505
|
+
edits: diffText(before, content)
|
|
2506
|
+
};
|
|
2507
|
+
if (index === -1) changes.push(change);
|
|
2508
|
+
else changes[index] = change;
|
|
2509
|
+
texts.set(path, before);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2370
2512
|
reviewChange() {
|
|
2371
2513
|
const reviews = this.state.reviews;
|
|
2372
2514
|
if (!reviews || !this.draft?.changed) return void 0;
|
|
@@ -2385,6 +2527,7 @@ var PlanBuilder = class {
|
|
|
2385
2527
|
};
|
|
2386
2528
|
function createPlanner(options) {
|
|
2387
2529
|
const texts = /* @__PURE__ */ new Map();
|
|
2530
|
+
let textsRevision;
|
|
2388
2531
|
/** Gives new reviews an id and a time, so planning again (after decisions) writes the same comments. */
|
|
2389
2532
|
function withReviewIds(ops, data) {
|
|
2390
2533
|
const used = /* @__PURE__ */ new Set();
|
|
@@ -2448,6 +2591,10 @@ function createPlanner(options) {
|
|
|
2448
2591
|
return {
|
|
2449
2592
|
plan(ops, answers = {}) {
|
|
2450
2593
|
const result = planWith(ops, answers);
|
|
2594
|
+
if (result.plan.revision !== textsRevision) {
|
|
2595
|
+
texts.clear();
|
|
2596
|
+
textsRevision = result.plan.revision;
|
|
2597
|
+
}
|
|
2451
2598
|
texts.set(result.plan.id, result.texts);
|
|
2452
2599
|
return result.plan;
|
|
2453
2600
|
},
|
|
@@ -2462,7 +2609,7 @@ function createPlanner(options) {
|
|
|
2462
2609
|
for (const change of fresh.plan.changes) {
|
|
2463
2610
|
const current = await options.host.readFile(change.path);
|
|
2464
2611
|
const expected = fresh.texts.get(change.path);
|
|
2465
|
-
if (change.type
|
|
2612
|
+
if (change.type !== "create" && expected !== void 0 && current !== expected || change.type === "create" && current !== void 0) throw errors.X18N4004({ path: change.path });
|
|
2466
2613
|
}
|
|
2467
2614
|
const events = [];
|
|
2468
2615
|
for (const change of fresh.plan.changes) events.push(await write(change));
|
|
@@ -2871,12 +3018,15 @@ function createProject(options) {
|
|
|
2871
3018
|
const source = message(key, sourceLocale)?.value;
|
|
2872
3019
|
return recorded !== void 0 && source !== void 0 && recorded === sourceHash(source);
|
|
2873
3020
|
}
|
|
2874
|
-
|
|
2875
|
-
|
|
3021
|
+
function finderContext() {
|
|
3022
|
+
return {
|
|
2876
3023
|
host,
|
|
2877
3024
|
config,
|
|
2878
3025
|
extensions: [...new Set(registry.formats.flatMap((format) => format.extensions))]
|
|
2879
3026
|
};
|
|
3027
|
+
}
|
|
3028
|
+
async function discover() {
|
|
3029
|
+
const ctx = finderContext();
|
|
2880
3030
|
const seen = /* @__PURE__ */ new Map();
|
|
2881
3031
|
const out = [];
|
|
2882
3032
|
duplicates = [];
|
|
@@ -2973,6 +3123,8 @@ function createProject(options) {
|
|
|
2973
3123
|
rawTree: {},
|
|
2974
3124
|
ranges: /* @__PURE__ */ new Map(),
|
|
2975
3125
|
descriptions: /* @__PURE__ */ new Map(),
|
|
3126
|
+
states: /* @__PURE__ */ new Map(),
|
|
3127
|
+
labels: /* @__PURE__ */ new Map(),
|
|
2976
3128
|
error: errors.X18N2001({ path: candidate.path })
|
|
2977
3129
|
};
|
|
2978
3130
|
try {
|
|
@@ -2991,6 +3143,8 @@ function createProject(options) {
|
|
|
2991
3143
|
rawTree: parsed.tree,
|
|
2992
3144
|
ranges: parsed.ranges ?? /* @__PURE__ */ new Map(),
|
|
2993
3145
|
descriptions,
|
|
3146
|
+
states: parsed.states ?? /* @__PURE__ */ new Map(),
|
|
3147
|
+
labels: parsed.labels ?? /* @__PURE__ */ new Map(),
|
|
2994
3148
|
error: void 0
|
|
2995
3149
|
};
|
|
2996
3150
|
} catch (error) {
|
|
@@ -3005,6 +3159,8 @@ function createProject(options) {
|
|
|
3005
3159
|
rawTree: {},
|
|
3006
3160
|
ranges: /* @__PURE__ */ new Map(),
|
|
3007
3161
|
descriptions: /* @__PURE__ */ new Map(),
|
|
3162
|
+
states: /* @__PURE__ */ new Map(),
|
|
3163
|
+
labels: /* @__PURE__ */ new Map(),
|
|
3008
3164
|
error: coded
|
|
3009
3165
|
};
|
|
3010
3166
|
}
|
|
@@ -3022,11 +3178,7 @@ function createProject(options) {
|
|
|
3022
3178
|
}
|
|
3023
3179
|
/** A path for a new resource, from the locale finders. */
|
|
3024
3180
|
function newResourcePath(locale, namespace, model) {
|
|
3025
|
-
const ctx =
|
|
3026
|
-
host,
|
|
3027
|
-
config,
|
|
3028
|
-
extensions: [...new Set(registry.formats.flatMap((format) => format.extensions))]
|
|
3029
|
-
};
|
|
3181
|
+
const ctx = finderContext();
|
|
3030
3182
|
for (const finder of registry.finders) {
|
|
3031
3183
|
const path = finder.resolveNew?.({
|
|
3032
3184
|
locale,
|
|
@@ -3036,6 +3188,19 @@ function createProject(options) {
|
|
|
3036
3188
|
if (path) return path;
|
|
3037
3189
|
}
|
|
3038
3190
|
}
|
|
3191
|
+
/** The file of one key, from the locale finders. */
|
|
3192
|
+
function keyFile(key, locale) {
|
|
3193
|
+
const finders = registry.finders.filter((finder) => finder.resolveKey);
|
|
3194
|
+
if (finders.length === 0) return void 0;
|
|
3195
|
+
const ctx = finderContext();
|
|
3196
|
+
for (const finder of finders) {
|
|
3197
|
+
const file = finder.resolveKey?.({
|
|
3198
|
+
key,
|
|
3199
|
+
locale
|
|
3200
|
+
}, ctx);
|
|
3201
|
+
if (file) return file;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3039
3204
|
async function loadResources(candidates, reread) {
|
|
3040
3205
|
const next = /* @__PURE__ */ new Map();
|
|
3041
3206
|
await forEachLimit([...new Set(candidates.map((c) => c.path))], ioConcurrency, async (path) => {
|
|
@@ -3165,7 +3330,7 @@ function createProject(options) {
|
|
|
3165
3330
|
usageEligible = (path) => registry.scannersFor(path).length > 0 && include(path) && !exclude(path);
|
|
3166
3331
|
await scanPaths((await host.glob(config.usage.include, {
|
|
3167
3332
|
ignore: config.usage.exclude,
|
|
3168
|
-
gitignore:
|
|
3333
|
+
gitignore: config.usage.gitignore
|
|
3169
3334
|
})).filter((path) => registry.scannersFor(path).length > 0));
|
|
3170
3335
|
}
|
|
3171
3336
|
let index;
|
|
@@ -3187,7 +3352,8 @@ function createProject(options) {
|
|
|
3187
3352
|
const path = [...stored, segment];
|
|
3188
3353
|
const key = keyFor(resource, path);
|
|
3189
3354
|
const storedPath = state.toStored(path);
|
|
3190
|
-
const
|
|
3355
|
+
const id = pathId(storedPath);
|
|
3356
|
+
const raw = state.ranges.get(id);
|
|
3191
3357
|
const range = raw ? {
|
|
3192
3358
|
key: shift(raw.key),
|
|
3193
3359
|
value: shift(raw.value)
|
|
@@ -3204,7 +3370,9 @@ function createProject(options) {
|
|
|
3204
3370
|
messages.set(key, byLocale);
|
|
3205
3371
|
const list = byLocale.get(resource.locale) ?? [];
|
|
3206
3372
|
byLocale.set(resource.locale, list);
|
|
3207
|
-
const description = state.descriptions.get(
|
|
3373
|
+
const description = state.descriptions.get(id);
|
|
3374
|
+
const translation = state.states.get(id);
|
|
3375
|
+
const label = state.labels.get(id);
|
|
3208
3376
|
list.push({
|
|
3209
3377
|
key,
|
|
3210
3378
|
locale: resource.locale,
|
|
@@ -3212,7 +3380,9 @@ function createProject(options) {
|
|
|
3212
3380
|
resource: resource.id,
|
|
3213
3381
|
storedPath,
|
|
3214
3382
|
range,
|
|
3215
|
-
...description === void 0 ? {} : { description }
|
|
3383
|
+
...description === void 0 ? {} : { description },
|
|
3384
|
+
...translation === void 0 ? {} : { state: translation },
|
|
3385
|
+
...label === void 0 ? {} : { label }
|
|
3216
3386
|
});
|
|
3217
3387
|
if (!keyPaths.has(key)) keyPaths.set(key, keyIdFor(resource, path));
|
|
3218
3388
|
} else {
|
|
@@ -3413,6 +3583,7 @@ function createProject(options) {
|
|
|
3413
3583
|
key,
|
|
3414
3584
|
namespace: id.namespace,
|
|
3415
3585
|
path: id.path,
|
|
3586
|
+
label: list.find((m) => m.label !== void 0)?.label,
|
|
3416
3587
|
messages: list,
|
|
3417
3588
|
missing: locales.filter((locale) => !byLocale.has(locale) && needs(key, locale)),
|
|
3418
3589
|
usages: usageMatches({ key }),
|
|
@@ -3565,6 +3736,7 @@ function createProject(options) {
|
|
|
3565
3736
|
basesOf,
|
|
3566
3737
|
toStored: (target, path) => storedPathOf(target)(path),
|
|
3567
3738
|
resolveNew: newResourcePath,
|
|
3739
|
+
resolveKey: keyFile,
|
|
3568
3740
|
update
|
|
3569
3741
|
});
|
|
3570
3742
|
const ready = (async () => {
|
|
@@ -3722,6 +3894,7 @@ function defaultDraft() {
|
|
|
3722
3894
|
usage: {
|
|
3723
3895
|
include: ["**/*"],
|
|
3724
3896
|
exclude: defaultExclude,
|
|
3897
|
+
gitignore: true,
|
|
3725
3898
|
calls: [],
|
|
3726
3899
|
regex: [],
|
|
3727
3900
|
ignoreCallees: [],
|
|
@@ -3748,7 +3921,12 @@ function defaultDraft() {
|
|
|
3748
3921
|
};
|
|
3749
3922
|
}
|
|
3750
3923
|
function serializablePart(config) {
|
|
3751
|
-
const { presets, plugins, pluginConfigs, history, ...
|
|
3924
|
+
const { presets, plugins, pluginConfigs, history, ignoreKeys: patterns, ...fields } = config;
|
|
3925
|
+
const ignoreKeys = patterns?.filter((pattern) => !(pattern instanceof RegExp));
|
|
3926
|
+
const rest = ignoreKeys === void 0 ? fields : {
|
|
3927
|
+
...fields,
|
|
3928
|
+
ignoreKeys
|
|
3929
|
+
};
|
|
3752
3930
|
if (typeof history !== "object") return {
|
|
3753
3931
|
...rest,
|
|
3754
3932
|
history
|
|
@@ -3823,6 +4001,7 @@ function applyLayer(draft, layer, origin, origins) {
|
|
|
3823
4001
|
draft.usage = {
|
|
3824
4002
|
include: usage.include ?? draft.usage.include,
|
|
3825
4003
|
exclude: [...draft.usage.exclude, ...usage.exclude ?? []],
|
|
4004
|
+
gitignore: usage.gitignore ?? draft.usage.gitignore,
|
|
3826
4005
|
calls: [...draft.usage.calls, ...usage.calls ?? []],
|
|
3827
4006
|
regex: [...draft.usage.regex, ...usage.regex ?? []],
|
|
3828
4007
|
ignoreCallees: [...draft.usage.ignoreCallees, ...usage.ignoreCallees ?? []],
|
package/package.json
CHANGED
package/schema/config.json
CHANGED
|
@@ -217,6 +217,9 @@
|
|
|
217
217
|
"type": "string"
|
|
218
218
|
}
|
|
219
219
|
},
|
|
220
|
+
"gitignore": {
|
|
221
|
+
"type": "boolean"
|
|
222
|
+
},
|
|
220
223
|
"calls": {
|
|
221
224
|
"type": "array",
|
|
222
225
|
"items": {
|
|
@@ -632,6 +635,18 @@
|
|
|
632
635
|
"type": "string"
|
|
633
636
|
}
|
|
634
637
|
},
|
|
638
|
+
"forms": {
|
|
639
|
+
"type": "object",
|
|
640
|
+
"propertyNames": {
|
|
641
|
+
"type": "string"
|
|
642
|
+
},
|
|
643
|
+
"additionalProperties": {
|
|
644
|
+
"type": "array",
|
|
645
|
+
"items": {
|
|
646
|
+
"type": "string"
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
},
|
|
635
650
|
"note": {
|
|
636
651
|
"type": "string"
|
|
637
652
|
},
|