@x18n/core 0.0.3 → 0.0.6

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
@@ -405,6 +405,37 @@ interface LocaleCoverage {
405
405
  readonly translated: number;
406
406
  readonly missing: readonly KeyString[];
407
407
  readonly empty: readonly KeyString[];
408
+ /** Translated keys whose message last changed before the source message. */
409
+ readonly outdated: readonly KeyString[];
410
+ }
411
+ /** When a message last changed, as a history provider knows it. */
412
+ interface MessageChange {
413
+ /** Milliseconds since the epoch. `Infinity` for a change not yet known to the provider. */
414
+ readonly updatedAt: number;
415
+ /** A provider revision, such as a commit sha. */
416
+ readonly revision?: string | undefined;
417
+ readonly author?: string | undefined;
418
+ /** The value the provider knows. A different current value is a local change. */
419
+ readonly value?: MessageLeaf | undefined;
420
+ }
421
+ interface MessageHistory {
422
+ readonly changes: ReadonlyMap<KeyString, ReadonlyMap<LocaleCode, MessageChange>>;
423
+ /** The value a message had at a time, for translate prompts of stale messages. */
424
+ readonly valueAt?: ((key: KeyString, locale: LocaleCode, at: number) => MessageLeaf | undefined) | undefined;
425
+ }
426
+ interface HistoryContext {
427
+ readonly host: Host;
428
+ readonly config: ResolvedConfig;
429
+ readonly resources: readonly Resource[];
430
+ /** Messages of a resource in another version of its file, read like the current file. Undefined for embedded resources and parse errors. */
431
+ readonly messagesIn: (resource: Resource, text: string) => ReadonlyMap<KeyString, MessageLeaf> | undefined;
432
+ }
433
+ interface HistoryProvider {
434
+ readonly id: string;
435
+ /** Reads when each message last changed. Undefined when the provider has nothing for this project. */
436
+ readonly load: (ctx: HistoryContext) => Promise<MessageHistory | undefined>;
437
+ /** Calls `onChange` when the history changes outside resource files, for example on a commit. */
438
+ readonly watch?: ((ctx: HistoryContext, onChange: () => void) => Disposable) | undefined;
408
439
  }
409
440
  interface CheckContext {
410
441
  readonly config: ResolvedConfig;
@@ -434,6 +465,8 @@ interface CheckContext {
434
465
  readonly hardStrings: () => Iterable<HardString & {
435
466
  readonly path: string;
436
467
  }>;
468
+ /** When a message last changed, from the history providers. Undefined until history is loaded. */
469
+ readonly history: (key: KeyString, locale: LocaleCode) => MessageChange | undefined;
437
470
  }
438
471
  interface Check {
439
472
  readonly id: string;
@@ -524,6 +557,7 @@ interface Plugin {
524
557
  readonly checks?: readonly Check[] | undefined;
525
558
  readonly reporters?: readonly Reporter[] | undefined;
526
559
  readonly placeholders?: readonly PlaceholderSyntax[] | undefined;
560
+ readonly histories?: readonly HistoryProvider[] | undefined;
527
561
  /** Turns raw key text into canonical form. The first plugin that returns a value wins. */
528
562
  readonly normalizeKey?: ((raw: string, ctx: KeyContext) => string | undefined) | undefined;
529
563
  readonly derivedKeys?: readonly string[] | undefined;
@@ -686,6 +720,8 @@ interface SerializableConfig {
686
720
  /** Keys (globs) excluded from all checks. */
687
721
  readonly ignoreKeys?: readonly string[] | undefined;
688
722
  readonly glossary?: readonly GlossaryEntry[] | undefined;
723
+ /** 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`). */
724
+ readonly history?: boolean | HistoryOptions | undefined;
689
725
  /** Monorepo: directories (globs) that are projects inheriting this config. */
690
726
  readonly projects?: readonly string[] | undefined;
691
727
  }
@@ -693,11 +729,22 @@ interface SerializableConfig {
693
729
  interface CheckIds {}
694
730
  /** Options of plugins under their own config key. Plugins add theirs through declaration merging. */
695
731
  interface PluginConfigs {}
732
+ /** Options of history providers under their own key, such as `git`. Providers add theirs through declaration merging. */
733
+ interface HistoryConfigs {}
734
+ type HistoryOptions = Partial<HistoryConfigs>;
735
+ interface HistorySettings extends HistoryOptions {
736
+ /** Providers asked before those of plugins. */
737
+ readonly providers: readonly HistoryProvider[];
738
+ }
696
739
  type PresetOption = Preset | PluginSpecifier | false | undefined;
697
740
  type PluginOption = Plugin | Preset | PluginSpecifier | false | undefined;
698
741
  /** The config accepted by `defineConfig`: the serializable part plus plugin objects. */
699
- interface UserConfig extends Omit<SerializableConfig, "presets" | "plugins" | "checks"> {
742
+ interface UserConfig extends Omit<SerializableConfig, "presets" | "plugins" | "checks" | "history"> {
700
743
  readonly presets?: "auto" | readonly PresetOption[] | undefined;
744
+ /** History providers for stale translations: `false` turns them off; provider options such as `git` sit under their key; `providers` adds providers asked first. */
745
+ readonly history?: boolean | (HistoryOptions & {
746
+ readonly providers?: readonly HistoryProvider[] | undefined;
747
+ }) | undefined;
701
748
  readonly plugins?: readonly PluginOption[] | undefined;
702
749
  readonly checks?: { readonly [K in keyof CheckIds]?: CheckSetting; } & {
703
750
  readonly [checkId: string]: CheckSetting;
@@ -742,6 +789,7 @@ interface ResolvedConfig {
742
789
  readonly strict: boolean;
743
790
  readonly ignoreKeys: readonly string[];
744
791
  readonly glossary: readonly GlossaryEntry[];
792
+ readonly history: false | HistorySettings;
745
793
  readonly pluginConfigs: Partial<PluginConfigs>;
746
794
  /** Origin of each top-level value, for `explain()`. */
747
795
  readonly origins: {
@@ -836,6 +884,7 @@ interface PluginRegistry {
836
884
  readonly placeholders: readonly PlaceholderSyntax[];
837
885
  readonly manifests: readonly ManifestReader[];
838
886
  readonly extractors: readonly Extractor[];
887
+ readonly histories: readonly HistoryProvider[];
839
888
  readonly conflicts: readonly CapabilityConflict[];
840
889
  /** Format for a file path: config `formats` first, then the first format that claims the extension. */
841
890
  readonly formatFor: (path: string, hint?: string) => Format | undefined;
@@ -856,6 +905,8 @@ interface PromptOptions {
856
905
  readonly cursor?: string | undefined;
857
906
  /** Include code around usages. Default true. */
858
907
  readonly context?: boolean | undefined;
908
+ /** Translate prompts: add stale translations (load history first). */
909
+ readonly stale?: boolean | undefined;
859
910
  }
860
911
  interface UsageContext {
861
912
  readonly path: string;
@@ -868,6 +919,8 @@ interface PromptItem {
868
919
  readonly placeholders: readonly string[];
869
920
  /** The current value in the target locale, when there is one. */
870
921
  readonly existing: string | undefined;
922
+ /** For a stale translation: the source value when the translation last changed, when known. */
923
+ readonly previousSource?: string | undefined;
871
924
  readonly context: readonly UsageContext[];
872
925
  }
873
926
  interface PromptData {
@@ -1116,6 +1169,14 @@ interface Project {
1116
1169
  readonly path: string;
1117
1170
  })[];
1118
1171
  readonly analyze: () => Analysis;
1172
+ /** Loads history from the history providers, after `ready`. `watch` reloads it when a provider sees a change. Analysis runs again once it is loaded. */
1173
+ readonly loadHistory: (options?: {
1174
+ readonly watch?: boolean | undefined;
1175
+ }) => Promise<void>;
1176
+ /** When a message last changed. Undefined until history is loaded, and for messages without history. */
1177
+ readonly history: (key: KeyString, locale: LocaleCode) => MessageChange | undefined;
1178
+ /** The value a message had at a time (ms), from the provider that knows the message. */
1179
+ readonly valueAt: (key: KeyString, locale: LocaleCode, at: number) => MessageLeaf | undefined;
1119
1180
  /** Parts of a translate or review prompt for an agent or LLM; render with `renderPrompt`. */
1120
1181
  readonly prompt: (kind: "translate" | "review", locale: LocaleCode, options?: PromptOptions) => PromptData;
1121
1182
  readonly explain: () => ProjectExplanation;
@@ -1252,4 +1313,4 @@ export declare function rangeContains(range: TextRange, offset: number): boolean
1252
1313
  /** Fast non-cryptographic string hash (cyrb53), as hex. */
1253
1314
  export declare function hashText(text: string): string;
1254
1315
  //#endregion
1255
- export type { Analysis, Answers, ApplyResult, ArgSource, Builtins, CacheStore, CallSpec, CapabilityConflict, Check, CheckContext, CheckIds, CheckLevel, CheckSetting, ConfigContext, ConfigDraft, ConfigOrigin, CreateProjectOptions, Decision, DecisionOption, DetectContext, Diagnostic, DiagnosticInput, Disposable, EditOp, Envelope, ErrorCode, ErrorInfo, ExtractItem, ExtractOptions, ExtractTemplate, Extractor, FileChange, FileEvent, FileScan, FinderContext, Fix, Format, FormatContext, GlobOptions, GlossaryEntry, HardString, Host, JsonObject, JsonPrimitive, JsonValue, KeyContext, KeyId, KeyInfo, KeyQuery, KeyString, KeyTarget, LineIndex, LinePosition, LocaleCode, LocaleCoverage, LocaleFinder, LocaleInfo, Location, Logger, ManifestReader, MemoryHost, Message, MessageLeaf, MessageOp, MessageTree, NamespaceFromPath, NamespaceOptions, NodeRange, OverlayHost, ParseResult, Placeholder, PlaceholderComparer, PlaceholderProblem, PlaceholderSyntax, Plan, Plugin, PluginConfigs, PluginFile, PluginOption, PluginRegistry, PluginSpecifier, Preset, PresetExplanation, PresetFactory, PresetOption, Project, ProjectChange, ProjectExplanation, PromptContext, PromptData, PromptDraft, PromptItem, PromptOptions, RegexScannerSpec, ReportInput, ReportSummary, Reporter, ReporterContext, ResolveConfigOptions, ResolvedConfig, Resource, ResourceBlock, ResourceCandidate, ResourceId, ResourceNode, ResourceSpec, ResourceTransform, ResourceTransformContext, ScanContext, ScanResult, Scanner, SchemaIssue, Scope, SerializableConfig, Setup, Severity, SourceFile, TextEdit, TextRange, TreeLeaf, Usage, UsageCandidate, UsageContext, UsageKind, UsageMatch, UsageOptions, UsageQuery, UserConfig, WireDiagnostic, WireRange, WriteOptions };
1316
+ export type { Analysis, Answers, ApplyResult, ArgSource, Builtins, CacheStore, CallSpec, CapabilityConflict, Check, CheckContext, CheckIds, CheckLevel, CheckSetting, ConfigContext, ConfigDraft, ConfigOrigin, CreateProjectOptions, Decision, DecisionOption, DetectContext, Diagnostic, DiagnosticInput, Disposable, EditOp, Envelope, ErrorCode, ErrorInfo, ExtractItem, ExtractOptions, ExtractTemplate, Extractor, FileChange, FileEvent, FileScan, FinderContext, Fix, Format, FormatContext, GlobOptions, GlossaryEntry, HardString, HistoryConfigs, HistoryContext, HistoryOptions, HistoryProvider, HistorySettings, Host, JsonObject, JsonPrimitive, JsonValue, KeyContext, KeyId, KeyInfo, KeyQuery, KeyString, KeyTarget, LineIndex, LinePosition, LocaleCode, LocaleCoverage, LocaleFinder, LocaleInfo, Location, Logger, ManifestReader, MemoryHost, Message, MessageChange, MessageHistory, MessageLeaf, MessageOp, MessageTree, NamespaceFromPath, NamespaceOptions, NodeRange, OverlayHost, ParseResult, Placeholder, PlaceholderComparer, PlaceholderProblem, PlaceholderSyntax, Plan, Plugin, PluginConfigs, PluginFile, PluginOption, PluginRegistry, PluginSpecifier, Preset, PresetExplanation, PresetFactory, PresetOption, Project, ProjectChange, ProjectExplanation, PromptContext, PromptData, PromptDraft, PromptItem, PromptOptions, RegexScannerSpec, ReportInput, ReportSummary, Reporter, ReporterContext, ResolveConfigOptions, ResolvedConfig, Resource, ResourceBlock, ResourceCandidate, ResourceId, ResourceNode, ResourceSpec, ResourceTransform, ResourceTransformContext, ScanContext, ScanResult, Scanner, SchemaIssue, Scope, SerializableConfig, Setup, Severity, SourceFile, TextEdit, TextRange, TreeLeaf, Usage, UsageCandidate, UsageContext, UsageKind, UsageMatch, UsageOptions, UsageQuery, UserConfig, WireDiagnostic, WireRange, WriteOptions };
package/dist/index.mjs CHANGED
@@ -395,7 +395,7 @@ function parseLocaleInfo(code) {
395
395
  const displayName = language ? safe(() => new Intl.DisplayNames(["en"], { type: "language" }).of(tag)) : void 0;
396
396
  const found = language ? safe(() => new Intl.PluralRules(tag).resolvedOptions().pluralCategories) : void 0;
397
397
  const pluralCategories = found ? pluralOrder.filter((category) => found.includes(category)) : ["other"];
398
- const flagRegion = region ?? (language ? defaultRegions[language] : void 0);
398
+ const flagRegion = region ?? (language === "zh" && script === "Hant" ? "TW" : language ? defaultRegions[language] : void 0);
399
399
  return {
400
400
  code,
401
401
  language,
@@ -583,6 +583,7 @@ function createRegistry(input, config) {
583
583
  placeholders: plugins.flatMap((plugin) => plugin.placeholders ?? []),
584
584
  manifests: plugins.flatMap((plugin) => plugin.manifests ?? []),
585
585
  extractors: plugins.flatMap((plugin) => plugin.extractors ?? []),
586
+ histories: plugins.flatMap((plugin) => plugin.histories ?? []),
586
587
  conflicts,
587
588
  formatById,
588
589
  formatFor(path, hint) {
@@ -734,6 +735,7 @@ const ConfigSchema = v.strictObject({
734
735
  strict: v.optional(v.boolean()),
735
736
  ignoreKeys: v.optional(strings),
736
737
  glossary: v.optional(v.array(glossaryEntry)),
738
+ history: v.optional(v.union([v.boolean(), v.record(v.string(), v.union([v.literal(false), v.looseObject({})]))])),
737
739
  projects: v.optional(strings)
738
740
  });
739
741
  const PluginFileSchema = v.strictObject({
@@ -854,6 +856,59 @@ function placeholderProblems(project, ops) {
854
856
  });
855
857
  }
856
858
  //#endregion
859
+ //#region src/project/history.ts
860
+ const localChange = { updatedAt: Number.POSITIVE_INFINITY };
861
+ /** A target message is stale when it last changed before its source message. */
862
+ function isOutdated(source, target) {
863
+ return source !== void 0 && target !== void 0 && target.updatedAt < source.updatedAt;
864
+ }
865
+ function createHistory(input) {
866
+ const { providers } = input;
867
+ let histories = [];
868
+ let loading;
869
+ let watchers;
870
+ async function reload() {
871
+ const ctx = input.context();
872
+ histories = (await Promise.all(providers.map((provider) => provider.load(ctx).catch((error) => {
873
+ input.logger?.warn(`History provider "${provider.id}" failed: ${error instanceof Error ? error.message : String(error)}`);
874
+ })))).filter((history) => history !== void 0);
875
+ input.onLoad();
876
+ }
877
+ const owner = (key, locale) => {
878
+ for (const history of histories) {
879
+ const change = history.changes.get(key)?.get(locale);
880
+ if (change) return {
881
+ history,
882
+ change
883
+ };
884
+ }
885
+ };
886
+ return {
887
+ load(options) {
888
+ if (providers.length === 0) return Promise.resolve();
889
+ loading ??= reload();
890
+ if (options.watch && !watchers) {
891
+ const ctx = input.context();
892
+ watchers = providers.flatMap((provider) => provider.watch ? [provider.watch(ctx, () => {
893
+ loading = reload();
894
+ })] : []);
895
+ }
896
+ return loading;
897
+ },
898
+ get(key, locale) {
899
+ const change = owner(key, locale)?.change;
900
+ if (!change) return void 0;
901
+ const current = input.current(key, locale);
902
+ return change.value !== void 0 && current !== void 0 && current !== change.value ? localChange : change;
903
+ },
904
+ valueAt: (key, locale, at) => owner(key, locale)?.history.valueAt?.(key, locale, at),
905
+ dispose() {
906
+ watchers?.forEach((watcher) => watcher.dispose());
907
+ watchers = void 0;
908
+ }
909
+ };
910
+ }
911
+ //#endregion
857
912
  //#region src/project/analysis.ts
858
913
  function levelOf(setting) {
859
914
  if (setting === void 0) return void 0;
@@ -866,21 +921,25 @@ function levelOf(setting) {
866
921
  options: setting[1]
867
922
  };
868
923
  }
869
- function computeCoverage(messages, locales, reference) {
870
- return locales.map((locale) => {
924
+ function computeCoverage(state, reference) {
925
+ const { messages, sourceLocale } = state;
926
+ return state.locales.map((locale) => {
871
927
  const missing = [];
872
928
  const empty = [];
929
+ const outdated = [];
873
930
  for (const key of reference) {
874
931
  const value = messages.get(key)?.get(locale)?.[0]?.value;
875
932
  if (value === void 0) missing.push(key);
876
933
  else if (value === "") empty.push(key);
934
+ else if (locale !== sourceLocale && isOutdated(state.history(key, sourceLocale), state.history(key, locale))) outdated.push(key);
877
935
  }
878
936
  return {
879
937
  locale,
880
938
  total: reference.length,
881
939
  translated: reference.length - missing.length - empty.length,
882
940
  missing,
883
- empty
941
+ empty,
942
+ outdated
884
943
  };
885
944
  });
886
945
  }
@@ -973,7 +1032,7 @@ function createAnalyzer(options) {
973
1032
  const { sourceLocale, locales, messages } = state;
974
1033
  const sourceKeys = [...messages.keys()].filter((key) => messages.get(key)?.has(sourceLocale)).sort();
975
1034
  const allKeys = [...messages.keys()].sort();
976
- const coverage = computeCoverage(messages, locales, sourceKeys.length > 0 ? sourceKeys : allKeys);
1035
+ const coverage = computeCoverage(state, sourceKeys.length > 0 ? sourceKeys : allKeys);
977
1036
  const isDefined = (key) => messages.get(key)?.has(sourceLocale) === true || state.parents.has(key) || (state.derived.get(key)?.length ?? 0) > 0 || sourceKeys.length === 0 && messages.has(key);
978
1037
  const hasScanners = state.files.size > 0;
979
1038
  const referenceSyntaxes = registry.placeholders.filter((syntax) => syntax.reference);
@@ -1021,6 +1080,7 @@ function createAnalyzer(options) {
1021
1080
  syntaxErrors: (value) => registry.placeholders.flatMap((syntax) => syntax.validate?.(value) ?? []),
1022
1081
  localeInfo: options.localeInfo,
1023
1082
  text: options.text,
1083
+ history: state.history,
1024
1084
  *hardStrings() {
1025
1085
  for (const [path, file] of state.files) for (const hard of file.result.hardStrings ?? []) yield {
1026
1086
  ...hard,
@@ -1702,13 +1762,15 @@ function buildPrompt(project, kind, to, options, extra) {
1702
1762
  const from = project.sourceLocale;
1703
1763
  const match = typeof options.keys === "string" ? keyMatcher([options.keys]) : void 0;
1704
1764
  const list = Array.isArray(options.keys) ? new Set(options.keys) : void 0;
1765
+ const stale = (key) => isOutdated(project.history(key, from), project.history(key, to));
1705
1766
  const candidates = project.keys({ namespace: options.namespace }).filter((key) => {
1706
1767
  if (match && !match(key)) return false;
1707
1768
  if (list && !list.has(key)) return false;
1708
1769
  const source = text(project.message(key, from)?.value);
1709
1770
  if (source === void 0 || source === "") return false;
1710
1771
  const target = text(project.message(key, to)?.value);
1711
- return kind === "translate" ? list !== void 0 || target === void 0 || target === "" : target !== void 0 && target !== "";
1772
+ if (kind === "review") return target !== void 0 && target !== "";
1773
+ return list !== void 0 || target === void 0 || target === "" || options.stale === true && stale(key);
1712
1774
  });
1713
1775
  const start = Number(options.cursor ?? 0) || 0;
1714
1776
  const limit = options.limit ?? 50;
@@ -1739,11 +1801,14 @@ function buildPrompt(project, kind, to, options, extra) {
1739
1801
  }).slice(0, 5) : [];
1740
1802
  const items = page.map((key) => {
1741
1803
  const source = text(project.message(key, from)?.value) ?? "";
1804
+ const translated = project.history(key, to);
1805
+ const previous = translated && stale(key) ? text(project.valueAt(key, from, translated.updatedAt)) : void 0;
1742
1806
  return {
1743
1807
  key,
1744
1808
  source,
1745
1809
  placeholders: [...new Set(syntaxes.flatMap((s) => s.find(source)).map((p) => p.text))],
1746
1810
  existing: text(project.message(key, to)?.value) || void 0,
1811
+ previousSource: previous === source ? void 0 : previous,
1747
1812
  context: options.context === false ? [] : contextOf(project, key)
1748
1813
  };
1749
1814
  });
@@ -1782,6 +1847,7 @@ function renderItem(item, to) {
1782
1847
  `- Source: ${JSON.stringify(item.source)}`
1783
1848
  ];
1784
1849
  if (item.placeholders.length) out.push(`- Placeholders: ${item.placeholders.map((p) => `\`${p}\``).join(", ")}`);
1850
+ if (item.previousSource !== void 0) out.push(`- Source when ${to} last changed: ${JSON.stringify(item.previousSource)}`);
1785
1851
  if (item.existing !== void 0) out.push(`- Current ${to}: ${JSON.stringify(item.existing)}`);
1786
1852
  for (const context of item.context) out.push(`- Used in \`${context.path}:${context.line}\`:`, "", " ```", ...context.snippet.split("\n").map((line) => ` ${line}`), " ```");
1787
1853
  out.push("");
@@ -2064,6 +2130,17 @@ function createProject(options) {
2064
2130
  };
2065
2131
  }
2066
2132
  }
2133
+ function messagesIn(resource, text) {
2134
+ if (resource.block) return void 0;
2135
+ const format = resource.format === "unknown" ? void 0 : resource.format;
2136
+ const state = parseResource({
2137
+ ...resource,
2138
+ format,
2139
+ block: void 0
2140
+ }, text);
2141
+ if (state.error) return void 0;
2142
+ return new Map([...walkLeaves(state.tree)].map((leaf) => [keyFor(state.resource, leaf.path), leaf.value]));
2143
+ }
2067
2144
  async function loadResources(candidates, reread) {
2068
2145
  const next = /* @__PURE__ */ new Map();
2069
2146
  const paths = [...new Set(candidates.map((c) => c.path))];
@@ -2304,6 +2381,25 @@ function createProject(options) {
2304
2381
  function message(key, locale) {
2305
2382
  return idx().messages.get(key)?.get(locale)?.[0];
2306
2383
  }
2384
+ const history = createHistory({
2385
+ providers: config.history === false ? [] : [...config.history.providers, ...registry.histories],
2386
+ context: () => ({
2387
+ host,
2388
+ config,
2389
+ resources: [...resourceStates.values()].map((state) => state.resource),
2390
+ messagesIn
2391
+ }),
2392
+ current: (key, locale) => message(key, locale)?.value,
2393
+ onLoad() {
2394
+ revision++;
2395
+ emit({
2396
+ revision,
2397
+ paths: [],
2398
+ keys: []
2399
+ });
2400
+ },
2401
+ logger: host.logger
2402
+ });
2307
2403
  function usageMatches(query = {}) {
2308
2404
  const { usages, usagesByKey, prefixUsages } = idx();
2309
2405
  const inPath = (usage) => query.path === void 0 || usage.path === query.path;
@@ -2356,7 +2452,8 @@ function createProject(options) {
2356
2452
  usagesByKey: idx().usagesByKey,
2357
2453
  prefixUsages: idx().prefixUsages,
2358
2454
  derived: idx().derived,
2359
- files
2455
+ files,
2456
+ history: history.get
2360
2457
  }),
2361
2458
  baseOf,
2362
2459
  localeInfo,
@@ -2541,16 +2638,17 @@ function createProject(options) {
2541
2638
  },
2542
2639
  update
2543
2640
  });
2641
+ const ready = (async () => {
2642
+ await loadResources(await discover(), "all");
2643
+ await scanAll();
2644
+ revision++;
2645
+ })();
2544
2646
  const project = {
2545
2647
  get name() {
2546
2648
  return config.name;
2547
2649
  },
2548
2650
  config,
2549
- ready: (async () => {
2550
- await loadResources(await discover(), "all");
2551
- await scanAll();
2552
- revision++;
2553
- })(),
2651
+ ready,
2554
2652
  get revision() {
2555
2653
  return revision;
2556
2654
  },
@@ -2606,6 +2704,12 @@ function createProject(options) {
2606
2704
  path: p
2607
2705
  }))),
2608
2706
  analyze: () => analyzer.analyze(),
2707
+ async loadHistory(historyOptions = {}) {
2708
+ await ready;
2709
+ await history.load(historyOptions);
2710
+ },
2711
+ history: history.get,
2712
+ valueAt: history.valueAt,
2609
2713
  prompt: (kind, locale, promptOptions = {}) => buildPrompt(project, kind, locale, promptOptions, {
2610
2714
  config,
2611
2715
  hooks: registry.plugins.flatMap((plugin) => plugin.translatePrompt ? [plugin.translatePrompt] : [])
@@ -2629,6 +2733,7 @@ function createProject(options) {
2629
2733
  },
2630
2734
  dispose() {
2631
2735
  watcher?.dispose();
2736
+ history.dispose();
2632
2737
  listeners.clear();
2633
2738
  }
2634
2739
  };
@@ -2681,12 +2786,21 @@ function defaultDraft() {
2681
2786
  strict: false,
2682
2787
  ignoreKeys: [],
2683
2788
  glossary: [],
2789
+ history: { providers: [] },
2684
2790
  pluginConfigs: {}
2685
2791
  };
2686
2792
  }
2687
2793
  function serializablePart(config) {
2688
- const { presets, plugins, pluginConfigs, ...rest } = config;
2689
- return rest;
2794
+ const { presets, plugins, pluginConfigs, history, ...rest } = config;
2795
+ if (typeof history !== "object") return {
2796
+ ...rest,
2797
+ history
2798
+ };
2799
+ const { providers, ...options } = history;
2800
+ return {
2801
+ ...rest,
2802
+ history: options
2803
+ };
2690
2804
  }
2691
2805
  /** Applies a user config layer: scalars replace, lists of rules append. */
2692
2806
  function applyLayer(draft, layer, origin, origins) {
@@ -2776,6 +2890,15 @@ function applyLayer(draft, layer, origin, origins) {
2776
2890
  draft.glossary = [...draft.glossary, ...layer.glossary];
2777
2891
  mark("glossary");
2778
2892
  }
2893
+ if (layer.history !== void 0) {
2894
+ const previous = draft.history || { providers: [] };
2895
+ draft.history = layer.history === false ? false : layer.history === true ? previous : {
2896
+ ...previous,
2897
+ ...layer.history,
2898
+ providers: [...previous.providers, ...layer.history.providers ?? []]
2899
+ };
2900
+ mark("history");
2901
+ }
2779
2902
  if (layer.pluginConfigs !== void 0) {
2780
2903
  draft.pluginConfigs = {
2781
2904
  ...draft.pluginConfigs,
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@x18n/core",
3
3
  "type": "module",
4
- "version": "0.0.3",
4
+ "version": "0.0.6",
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",
8
- "homepage": "https://github.com/antfu/x18n#readme",
8
+ "homepage": "https://github.com/x18n-dev/x18n#readme",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/antfu/x18n.git",
11
+ "url": "git+https://github.com/x18n-dev/x18n.git",
12
12
  "directory": "packages/core"
13
13
  },
14
- "bugs": "https://github.com/antfu/x18n/issues",
14
+ "bugs": "https://github.com/x18n-dev/x18n/issues",
15
15
  "sideEffects": false,
16
16
  "exports": {
17
17
  ".": "./dist/index.mjs",
@@ -522,6 +522,31 @@
522
522
  "additionalProperties": false
523
523
  }
524
524
  },
525
+ "history": {
526
+ "anyOf": [
527
+ {
528
+ "type": "boolean"
529
+ },
530
+ {
531
+ "type": "object",
532
+ "propertyNames": {
533
+ "type": "string"
534
+ },
535
+ "additionalProperties": {
536
+ "anyOf": [
537
+ {
538
+ "const": false
539
+ },
540
+ {
541
+ "type": "object",
542
+ "properties": {},
543
+ "required": []
544
+ }
545
+ ]
546
+ }
547
+ }
548
+ ]
549
+ },
525
550
  "projects": {
526
551
  "type": "array",
527
552
  "items": {