@x18n/core 0.1.7 → 0.1.8

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 {
@@ -775,11 +798,15 @@ interface GlossaryEntry {
775
798
  readonly translations?: {
776
799
  readonly [locale: string]: string;
777
800
  } | undefined;
801
+ /** 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. */
802
+ readonly forms?: {
803
+ readonly [locale: string]: readonly string[];
804
+ } | undefined;
778
805
  readonly note?: string | undefined;
779
806
  readonly doNotTranslate?: boolean | undefined;
780
807
  }
781
808
  interface ExtractTemplate {
782
- /** Replacement code with `{key}`, `{attr}`, `{args}`, `{text}`, `{source}` and `{parts}` tokens. `{key}` and `{text}` are escaped for the quotes around them. */
809
+ /** 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
810
  readonly template: string;
784
811
  /** Hard-string kinds the template applies to, for example `jsx-text`. */
785
812
  readonly kinds?: readonly string[] | undefined;
@@ -813,6 +840,8 @@ interface ExtractOptions {
813
840
  interface UsageOptions {
814
841
  readonly include?: readonly string[] | undefined;
815
842
  readonly exclude?: readonly string[] | undefined;
843
+ /** 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. */
844
+ readonly gitignore?: boolean | undefined;
816
845
  readonly calls?: readonly CallSpec[] | undefined;
817
846
  readonly regex?: readonly RegexScannerSpec[] | undefined;
818
847
  /** Callee names never counted as translate calls. */
@@ -952,6 +981,7 @@ interface ResolvedConfig {
952
981
  readonly usage: {
953
982
  readonly include: readonly string[];
954
983
  readonly exclude: readonly string[];
984
+ readonly gitignore: boolean;
955
985
  readonly calls: readonly CallSpec[];
956
986
  readonly regex: readonly RegexScannerSpec[];
957
987
  readonly ignoreCallees: readonly string[];
@@ -1051,6 +1081,8 @@ interface Resource {
1051
1081
  /** Key separator of the resource when it differs from the config's `keySeparator` (from its resource spec). */
1052
1082
  readonly keySeparator?: KeySeparator | undefined;
1053
1083
  }
1084
+ /** How far a translation got, as in XLIFF: a value can exist before anyone reviews it. */
1085
+ type MessageState = "translated" | "needs-review" | "reviewed";
1054
1086
  /** One stored value of one key in one locale. */
1055
1087
  interface Message {
1056
1088
  readonly key: KeyString;
@@ -1063,6 +1095,10 @@ interface Message {
1063
1095
  readonly range: NodeRange | undefined;
1064
1096
  /** What the resource says about the message for translators, such as a note or a comment beside it. */
1065
1097
  readonly description?: string | undefined;
1098
+ /** The translation state the resource records, such as XLIFF `state` or PO `fuzzy`. */
1099
+ readonly state?: MessageState | undefined;
1100
+ /** A display label of the key from the resource, for keys that do not read well, such as hashes. */
1101
+ readonly label?: string | undefined;
1066
1102
  }
1067
1103
  type UsageKind = "call" | "selector" | "accessor" | "constant" | "marker";
1068
1104
  interface Usage {
@@ -1523,6 +1559,8 @@ interface KeyInfo {
1523
1559
  readonly key: KeyString;
1524
1560
  readonly namespace: string | undefined;
1525
1561
  readonly path: readonly string[];
1562
+ /** Display label of the key: the first `label` of its messages, source locale first. */
1563
+ readonly label: string | undefined;
1526
1564
  /** One message per locale that has the key, source locale first. */
1527
1565
  readonly messages: readonly Message[];
1528
1566
  readonly missing: readonly LocaleCode[];
@@ -1876,4 +1914,4 @@ export declare function rangeContains(range: TextRange, offset: number): boolean
1876
1914
  /** Fast non-cryptographic string hash (cyrb53), as hex. */
1877
1915
  export declare function hashText(text: string): string;
1878
1916
  //#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 };
1917
+ export type { Analysis, Answers, ApplyResult, ArgSource, BaselineSummary, Builtins, CacheStore, CallSpec, CapabilityConflict, Check, CheckContext, CheckIds, CheckLevel, CheckSetting, ConfigContext, ConfigDraft, ConfigOrigin, CreateProjectOptions, Decision, DecisionOption, DetectContext, Diagnostic, DiagnosticInput, Disposable, EditOp, Envelope, ErrorCode, ErrorInfo, ExtractItem, ExtractOptions, ExtractTemplate, Extractor, FileChange, FileEvent, FileScan, FinderContext, Fix, Format, FormatContext, FormatEntry, GlobOptions, GlossaryEntry, HardString, HistoryConfigs, HistoryContext, HistoryOptions, HistoryProvider, HistorySettings, Host, JsonObject, JsonPrimitive, JsonValue, KeptUnusedKey, KeyContext, KeyFile, KeyId, KeyInfo, KeyQuery, KeySeparator, KeyString, KeyTarget, LazyFormat, LazyScanner, LibraryOptions, LineIndex, LinePosition, LocaleCode, LocaleCoverage, LocaleFinder, LocaleInfo, Location, Logger, ManifestReader, MemoryHost, Message, MessageChange, MessageHistory, MessageLeaf, MessageOp, MessageState, MessageTree, NamespaceFromPath, NamespaceOptions, NodeRange, OverlayHost, ParamsSource, ParseResult, Placeholder, PlaceholderComparer, PlaceholderProblem, PlaceholderSyntax, Plan, Plugin, PluginConfigs, PluginFile, PluginLoader, PluginOption, PluginRegistry, PluginSpecifier, PluralForm, Preset, PresetDetection, PresetEntry, PresetExplanation, PresetFactory, PresetLoader, PresetOption, Project, ProjectChange, ProjectExplanation, PromptContext, PromptData, PromptDraft, PromptIssue, PromptItem, PromptOptions, RegexScannerSpec, RelatedValue, ReportInput, ReportSummary, Reporter, ReporterContext, ReporterLoader, ResolveConfigOptions, ResolvedConfig, Resource, ResourceBlock, ResourceCandidate, ResourceId, ResourceNode, ResourceSpec, ResourceTransform, ResourceTransformContext, ReviewAuthor, ReviewComment, ReviewData, ReviewFile, ReviewFilter, ReviewOptions, ReviewState, ReviewStore, ReviewThread, ReviewType, ScanContext, ScanResult, Scanner, ScannerEntry, SchemaIssue, Scope, SearchMatch, SearchOptions, SerializableConfig, Setup, Severity, SourceFile, TextEdit, TextRange, ThreadComment, TreeLeaf, UnusedDeletion, UnusedKeyDeletion, UnusedQuery, Usage, UsageCandidate, UsageContext, UsageKind, UsageMatch, UsageOptions, UsageQuery, UserConfig, WireDiagnostic, WireRange, WriteOptions, X18nError };
package/dist/index.mjs CHANGED
@@ -882,6 +882,7 @@ const extractOptions = v.strictObject({
882
882
  const glossaryEntry = v.strictObject({
883
883
  term: v.string(),
884
884
  translations: v.optional(v.record(v.string(), v.string())),
885
+ forms: v.optional(v.record(v.string(), v.array(v.string()))),
885
886
  note: v.optional(v.string()),
886
887
  doNotTranslate: v.optional(v.boolean())
887
888
  });
@@ -924,6 +925,7 @@ const ConfigSchema = v.strictObject({
924
925
  usage: v.optional(v.strictObject({
925
926
  include: v.optional(strings),
926
927
  exclude: v.optional(strings),
928
+ gitignore: v.optional(v.boolean()),
927
929
  calls: v.optional(v.array(callSpec)),
928
930
  regex: v.optional(v.array(regexSpec)),
929
931
  ignoreCallees: v.optional(strings),
@@ -1370,12 +1372,19 @@ function computeCoverage(state, options, sourceKeys) {
1370
1372
  const missing = [];
1371
1373
  const empty = [];
1372
1374
  const outdated = [];
1375
+ const needsReview = [];
1376
+ const reviewed = [];
1373
1377
  for (const key of reference) {
1374
- const value = messages.get(key)?.get(locale)?.[0]?.value;
1378
+ const message = messages.get(key)?.get(locale)?.[0];
1379
+ const value = message?.value;
1375
1380
  if (value === void 0 || value === "") {
1376
1381
  if (inherited(key)) continue;
1377
1382
  (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);
1383
+ continue;
1384
+ }
1385
+ if (locale !== sourceLocale && isOutdated(state.history(key, sourceLocale), state.history(key, locale)) && !state.fresh(key, locale)) outdated.push(key);
1386
+ if (message?.state === "needs-review") needsReview.push(key);
1387
+ else if (message?.state === "reviewed") reviewed.push(key);
1379
1388
  }
1380
1389
  return {
1381
1390
  locale,
@@ -1383,7 +1392,9 @@ function computeCoverage(state, options, sourceKeys) {
1383
1392
  translated: reference.length - missing.length - empty.length,
1384
1393
  missing,
1385
1394
  empty,
1386
- outdated
1395
+ outdated,
1396
+ needsReview,
1397
+ reviewed
1387
1398
  };
1388
1399
  });
1389
1400
  }
@@ -1771,6 +1782,10 @@ var PlanBuilder = class {
1771
1782
  answers;
1772
1783
  work = /* @__PURE__ */ new Map();
1773
1784
  created = /* @__PURE__ */ new Map();
1785
+ /** New files that a finder's `resolveKey` gave one key. */
1786
+ keyFiles = /* @__PURE__ */ new Map();
1787
+ /** Resources that are the file of a key deleted from them: removed once empty. */
1788
+ keyFilesLeft = /* @__PURE__ */ new Set();
1774
1789
  sourceEdits = /* @__PURE__ */ new Map();
1775
1790
  sourceImports = /* @__PURE__ */ new Map();
1776
1791
  decisions = [];
@@ -1898,8 +1913,40 @@ var PlanBuilder = class {
1898
1913
  this.created.set(path, target);
1899
1914
  return target;
1900
1915
  }
1901
- /** Placement: the namespace's resource, then the longest shared prefix, then the first; a tie is a decision. */
1916
+ /** The file a locale finder gives the key, created when it does not exist. */
1917
+ keyTarget(key, locale) {
1918
+ const file = this.options.resolveKey(key, locale);
1919
+ if (!file) return void 0;
1920
+ const existing = this.state.resources.find((s) => s.resource.path === file.path && !s.resource.block);
1921
+ if (existing) {
1922
+ if (existing.resource.locale !== locale || this.relativePath(key, existing.resource) === void 0) throw errors.X18N4005({
1923
+ key,
1924
+ locale
1925
+ });
1926
+ return this.targetOf(existing);
1927
+ }
1928
+ const pending = this.keyFiles.get(file.path);
1929
+ if (pending) return pending;
1930
+ const format = file.format === void 0 ? this.options.registry.formatFor(file.path) : this.options.registry.formatById(file.format);
1931
+ if (!format?.serialize) throw errors.X18N4005({
1932
+ key,
1933
+ locale
1934
+ });
1935
+ const target = {
1936
+ id: file.path,
1937
+ path: file.path,
1938
+ locale,
1939
+ namespace: this.state.namespaced ? this.keyId(key).namespace ?? this.config.namespace.default : void 0,
1940
+ format,
1941
+ state: void 0
1942
+ };
1943
+ this.keyFiles.set(file.path, target);
1944
+ return target;
1945
+ }
1946
+ /** 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
1947
  pickTarget(key, locale) {
1948
+ const own = this.keyTarget(key, locale);
1949
+ if (own) return own;
1903
1950
  let inLocale = this.state.resources.filter((s) => s.resource.locale === locale && this.relativePath(key, s.resource) !== void 0);
1904
1951
  const source = this.options.message(key, this.state.sourceLocale);
1905
1952
  const sourceNamespace = source && this.stateById.get(source.resource)?.resource.namespace;
@@ -1958,7 +2005,9 @@ var PlanBuilder = class {
1958
2005
  });
1959
2006
  }
1960
2007
  deleteMessage(m) {
1961
- const entry = this.workFor(this.targetOf(this.writable(m.resource)));
2008
+ const s = this.writable(m.resource);
2009
+ if (!s.resource.block && this.options.resolveKey(m.key, m.locale)?.path === s.resource.path) this.keyFilesLeft.add(s.resource.id);
2010
+ const entry = this.workFor(this.targetOf(s));
1962
2011
  let path = [...m.storedPath];
1963
2012
  while (path.length > 1) {
1964
2013
  const parent = nodeAt(entry.tree, path.slice(0, -1));
@@ -2017,7 +2066,8 @@ var PlanBuilder = class {
2017
2066
  if (renamed.has(m.resource)) continue;
2018
2067
  const s = this.writable(m.resource);
2019
2068
  const relative = this.relativePath(to, s.resource);
2020
- if (!relative) {
2069
+ const own = this.options.resolveKey(to, locale);
2070
+ if (!relative || own && own.path !== s.resource.path) {
2021
2071
  this.deleteMessage(m);
2022
2072
  this.setValue(to, locale, m.value);
2023
2073
  continue;
@@ -2326,6 +2376,14 @@ var PlanBuilder = class {
2326
2376
  texts.set(target.path, void 0);
2327
2377
  continue;
2328
2378
  }
2379
+ if (this.keyFilesLeft.has(s.resource.id) && Object.keys(entry.tree).length === 0) {
2380
+ changes.push({
2381
+ type: "delete",
2382
+ path: target.path
2383
+ });
2384
+ texts.set(target.path, this.state.fileTexts.get(target.path));
2385
+ continue;
2386
+ }
2329
2387
  const edits = diffText(s.content, this.nextContent(entry, s)).map((e) => ({
2330
2388
  range: {
2331
2389
  start: e.range.start + s.offset,
@@ -2462,7 +2520,7 @@ function createPlanner(options) {
2462
2520
  for (const change of fresh.plan.changes) {
2463
2521
  const current = await options.host.readFile(change.path);
2464
2522
  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 });
2523
+ if (change.type !== "create" && expected !== void 0 && current !== expected || change.type === "create" && current !== void 0) throw errors.X18N4004({ path: change.path });
2466
2524
  }
2467
2525
  const events = [];
2468
2526
  for (const change of fresh.plan.changes) events.push(await write(change));
@@ -2871,12 +2929,15 @@ function createProject(options) {
2871
2929
  const source = message(key, sourceLocale)?.value;
2872
2930
  return recorded !== void 0 && source !== void 0 && recorded === sourceHash(source);
2873
2931
  }
2874
- async function discover() {
2875
- const ctx = {
2932
+ function finderContext() {
2933
+ return {
2876
2934
  host,
2877
2935
  config,
2878
2936
  extensions: [...new Set(registry.formats.flatMap((format) => format.extensions))]
2879
2937
  };
2938
+ }
2939
+ async function discover() {
2940
+ const ctx = finderContext();
2880
2941
  const seen = /* @__PURE__ */ new Map();
2881
2942
  const out = [];
2882
2943
  duplicates = [];
@@ -2973,6 +3034,8 @@ function createProject(options) {
2973
3034
  rawTree: {},
2974
3035
  ranges: /* @__PURE__ */ new Map(),
2975
3036
  descriptions: /* @__PURE__ */ new Map(),
3037
+ states: /* @__PURE__ */ new Map(),
3038
+ labels: /* @__PURE__ */ new Map(),
2976
3039
  error: errors.X18N2001({ path: candidate.path })
2977
3040
  };
2978
3041
  try {
@@ -2991,6 +3054,8 @@ function createProject(options) {
2991
3054
  rawTree: parsed.tree,
2992
3055
  ranges: parsed.ranges ?? /* @__PURE__ */ new Map(),
2993
3056
  descriptions,
3057
+ states: parsed.states ?? /* @__PURE__ */ new Map(),
3058
+ labels: parsed.labels ?? /* @__PURE__ */ new Map(),
2994
3059
  error: void 0
2995
3060
  };
2996
3061
  } catch (error) {
@@ -3005,6 +3070,8 @@ function createProject(options) {
3005
3070
  rawTree: {},
3006
3071
  ranges: /* @__PURE__ */ new Map(),
3007
3072
  descriptions: /* @__PURE__ */ new Map(),
3073
+ states: /* @__PURE__ */ new Map(),
3074
+ labels: /* @__PURE__ */ new Map(),
3008
3075
  error: coded
3009
3076
  };
3010
3077
  }
@@ -3022,11 +3089,7 @@ function createProject(options) {
3022
3089
  }
3023
3090
  /** A path for a new resource, from the locale finders. */
3024
3091
  function newResourcePath(locale, namespace, model) {
3025
- const ctx = {
3026
- host,
3027
- config,
3028
- extensions: [...new Set(registry.formats.flatMap((format) => format.extensions))]
3029
- };
3092
+ const ctx = finderContext();
3030
3093
  for (const finder of registry.finders) {
3031
3094
  const path = finder.resolveNew?.({
3032
3095
  locale,
@@ -3036,6 +3099,19 @@ function createProject(options) {
3036
3099
  if (path) return path;
3037
3100
  }
3038
3101
  }
3102
+ /** The file of one key, from the locale finders. */
3103
+ function keyFile(key, locale) {
3104
+ const finders = registry.finders.filter((finder) => finder.resolveKey);
3105
+ if (finders.length === 0) return void 0;
3106
+ const ctx = finderContext();
3107
+ for (const finder of finders) {
3108
+ const file = finder.resolveKey?.({
3109
+ key,
3110
+ locale
3111
+ }, ctx);
3112
+ if (file) return file;
3113
+ }
3114
+ }
3039
3115
  async function loadResources(candidates, reread) {
3040
3116
  const next = /* @__PURE__ */ new Map();
3041
3117
  await forEachLimit([...new Set(candidates.map((c) => c.path))], ioConcurrency, async (path) => {
@@ -3165,7 +3241,7 @@ function createProject(options) {
3165
3241
  usageEligible = (path) => registry.scannersFor(path).length > 0 && include(path) && !exclude(path);
3166
3242
  await scanPaths((await host.glob(config.usage.include, {
3167
3243
  ignore: config.usage.exclude,
3168
- gitignore: true
3244
+ gitignore: config.usage.gitignore
3169
3245
  })).filter((path) => registry.scannersFor(path).length > 0));
3170
3246
  }
3171
3247
  let index;
@@ -3187,7 +3263,8 @@ function createProject(options) {
3187
3263
  const path = [...stored, segment];
3188
3264
  const key = keyFor(resource, path);
3189
3265
  const storedPath = state.toStored(path);
3190
- const raw = state.ranges.get(pathId(storedPath));
3266
+ const id = pathId(storedPath);
3267
+ const raw = state.ranges.get(id);
3191
3268
  const range = raw ? {
3192
3269
  key: shift(raw.key),
3193
3270
  value: shift(raw.value)
@@ -3204,7 +3281,9 @@ function createProject(options) {
3204
3281
  messages.set(key, byLocale);
3205
3282
  const list = byLocale.get(resource.locale) ?? [];
3206
3283
  byLocale.set(resource.locale, list);
3207
- const description = state.descriptions.get(pathId(storedPath));
3284
+ const description = state.descriptions.get(id);
3285
+ const translation = state.states.get(id);
3286
+ const label = state.labels.get(id);
3208
3287
  list.push({
3209
3288
  key,
3210
3289
  locale: resource.locale,
@@ -3212,7 +3291,9 @@ function createProject(options) {
3212
3291
  resource: resource.id,
3213
3292
  storedPath,
3214
3293
  range,
3215
- ...description === void 0 ? {} : { description }
3294
+ ...description === void 0 ? {} : { description },
3295
+ ...translation === void 0 ? {} : { state: translation },
3296
+ ...label === void 0 ? {} : { label }
3216
3297
  });
3217
3298
  if (!keyPaths.has(key)) keyPaths.set(key, keyIdFor(resource, path));
3218
3299
  } else {
@@ -3413,6 +3494,7 @@ function createProject(options) {
3413
3494
  key,
3414
3495
  namespace: id.namespace,
3415
3496
  path: id.path,
3497
+ label: list.find((m) => m.label !== void 0)?.label,
3416
3498
  messages: list,
3417
3499
  missing: locales.filter((locale) => !byLocale.has(locale) && needs(key, locale)),
3418
3500
  usages: usageMatches({ key }),
@@ -3565,6 +3647,7 @@ function createProject(options) {
3565
3647
  basesOf,
3566
3648
  toStored: (target, path) => storedPathOf(target)(path),
3567
3649
  resolveNew: newResourcePath,
3650
+ resolveKey: keyFile,
3568
3651
  update
3569
3652
  });
3570
3653
  const ready = (async () => {
@@ -3722,6 +3805,7 @@ function defaultDraft() {
3722
3805
  usage: {
3723
3806
  include: ["**/*"],
3724
3807
  exclude: defaultExclude,
3808
+ gitignore: true,
3725
3809
  calls: [],
3726
3810
  regex: [],
3727
3811
  ignoreCallees: [],
@@ -3823,6 +3907,7 @@ function applyLayer(draft, layer, origin, origins) {
3823
3907
  draft.usage = {
3824
3908
  include: usage.include ?? draft.usage.include,
3825
3909
  exclude: [...draft.usage.exclude, ...usage.exclude ?? []],
3910
+ gitignore: usage.gitignore ?? draft.usage.gitignore,
3826
3911
  calls: [...draft.usage.calls, ...usage.calls ?? []],
3827
3912
  regex: [...draft.usage.regex, ...usage.regex ?? []],
3828
3913
  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.8",
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
  },