@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 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 with `{key}`, `{attr}`, `{args}`, `{text}`, `{source}` and `{parts}` tokens. `{key}` and `{text}` are escaped for the quotes around them. */
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 string[];
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
- /** Matcher over key globs: `*` stays inside a segment, `**` spans segments. With literal keys (`separator: false`) `*` spans the whole key. */
1251
- export declare function keyMatcher(globs: readonly string[], separator?: KeySeparator): (key: KeyString) => boolean;
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
- /** Matcher over key globs: `*` stays inside a segment, `**` spans segments. With literal keys (`separator: false`) `*` spans the whole key. */
383
- function keyMatcher(globs, separator = ".") {
384
- if (globs.length === 0) return () => false;
385
- const regexes = globs.map((glob) => globToRegExp(glob, separator === false ? "\0" : separator));
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
- for (const op of ops) if (op.type === "set") setIn(out, op.path, op.value);
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 value = messages.get(key)?.get(locale)?.[0]?.value;
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
- } else if (locale !== sourceLocale && isOutdated(state.history(key, sourceLocale), state.history(key, locale)) && !state.fresh(key, locale)) outdated.push(key);
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
- return stored.length === relative.length && (isFlatTree(entry.tree, separator) ?? this.config.keyStyle === "flat") ? [stored.join(separator)] : stored;
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
- /** Placement: the namespace's resource, then the longest shared prefix, then the first; a tie is a decision. */
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
- entry.tree = applyMessageOps(entry.tree, [op]);
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.pushOp(this.workFor(this.targetOf(this.writable(existing.resource))), {
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 entry = this.workFor(this.targetOf(this.writable(m.resource)));
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
- entry.tree = applyMessageOps(entry.tree, [...leaves.map((leaf) => ({
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 [locale, list] of this.options.messages(from) ?? []) {
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 s = this.writable(m.resource);
2019
- const relative = this.relativePath(to, s.resource);
2020
- if (!relative) {
2021
- this.deleteMessage(m);
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.pushOp(entry, {
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: this.storedPathFor(entry, relative),
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 === "edit" && expected !== void 0 && current !== expected || change.type === "create" && current !== void 0) throw errors.X18N4004({ path: change.path });
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
- async function discover() {
2875
- const ctx = {
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: true
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 raw = state.ranges.get(pathId(storedPath));
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(pathId(storedPath));
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, ...rest } = config;
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@x18n/core",
3
3
  "type": "module",
4
- "version": "0.1.7",
4
+ "version": "0.1.9",
5
5
  "description": "Runtime-neutral engine of x18n: domain model, plugin API and project analysis",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -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
  },