@x18n/core 0.0.13 → 0.0.14

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
@@ -434,10 +434,14 @@ interface LocaleFinder {
434
434
  readonly id: string;
435
435
  readonly override?: boolean | undefined;
436
436
  readonly find: (ctx: FinderContext) => Promise<readonly ResourceCandidate[]>;
437
- /** Where a new resource for (locale, namespace) should go. Used by edits that create files. */
437
+ /**
438
+ * Where a new resource for (locale, namespace) should go. Used by edits that create files.
439
+ * `model` is the existing resource the new one would otherwise mirror: the key's source file when it has one.
440
+ */
438
441
  readonly resolveNew?: ((target: {
439
442
  readonly locale: LocaleCode;
440
443
  readonly namespace: string | undefined;
444
+ readonly model: Resource | undefined;
441
445
  }, ctx: FinderContext) => string | undefined) | undefined;
442
446
  }
443
447
  interface FormatContext {
@@ -459,6 +463,28 @@ type MessageOp = {
459
463
  readonly type: "delete";
460
464
  readonly path: readonly string[];
461
465
  };
466
+ /** A built-in format known by its extensions only: the module behind `load` is imported when a file of that format is read. */
467
+ interface FormatEntry {
468
+ /** File extensions without the dot. */
469
+ readonly extensions: readonly string[];
470
+ /** The module's default export creates a plugin that holds the format. */
471
+ readonly load: PluginLoader;
472
+ }
473
+ /** A format entry under its id, as plugins list it. */
474
+ interface LazyFormat extends FormatEntry {
475
+ readonly id: string;
476
+ }
477
+ /** A built-in scanner known by its extensions only: the module behind `load` is imported when a file it scans is first scanned. */
478
+ interface ScannerEntry {
479
+ /** File extensions without dot. `*` matches every file. */
480
+ readonly extensions: readonly string[];
481
+ /** The module's default export creates a plugin that holds the scanner. */
482
+ readonly load: PluginLoader;
483
+ }
484
+ /** A scanner entry under its id, as plugins list it. */
485
+ interface LazyScanner extends ScannerEntry {
486
+ readonly id: string;
487
+ }
462
488
  interface Format {
463
489
  readonly id: string;
464
490
  /** File extensions without the dot. */
@@ -496,6 +522,8 @@ interface Scope {
496
522
  readonly range: TextRange;
497
523
  readonly namespace?: string | undefined;
498
524
  readonly keyPrefix?: string | undefined;
525
+ /** The key prefix applies only to keys it defines; others resolve without it, as page messages that extend global ones. */
526
+ readonly optionalPrefix?: boolean | undefined;
499
527
  }
500
528
  interface HardString {
501
529
  readonly text: string;
@@ -703,6 +731,10 @@ interface Plugin {
703
731
  readonly manifests?: readonly ManifestReader[] | undefined;
704
732
  readonly localeFinders?: readonly LocaleFinder[] | undefined;
705
733
  readonly formats?: readonly Format[] | undefined;
734
+ /** Formats that load their module when a file of that format is first read. */
735
+ readonly lazyFormats?: readonly LazyFormat[] | undefined;
736
+ /** Scanners that load their module when a file they scan is first scanned. */
737
+ readonly lazyScanners?: readonly LazyScanner[] | undefined;
706
738
  readonly scanners?: readonly Scanner[] | undefined;
707
739
  readonly extractors?: readonly Extractor[] | undefined;
708
740
  readonly checks?: readonly Check[] | undefined;
@@ -727,6 +759,32 @@ interface Preset {
727
759
  }
728
760
  /** A preset factory as referenced by name from YAML and JSON configs. */
729
761
  type PresetFactory = (options?: JsonObject) => Preset;
762
+ /** Loads a plugin on demand, usually `() => import('./plugin')`: the module's default export creates it. */
763
+ type PluginLoader = () => Promise<{
764
+ readonly default: () => Plugin;
765
+ }>;
766
+ /** Loads a reporter on demand, usually `() => import('./reporter')`: the module's default export is the reporter. */
767
+ type ReporterLoader = () => Promise<{
768
+ readonly default: Reporter;
769
+ }>;
770
+ /** Loads a preset on demand, usually `() => import('./preset')`: the module's default export is the factory. */
771
+ type PresetLoader = () => Promise<{
772
+ readonly default: PresetFactory;
773
+ }>;
774
+ /** When a built-in preset applies. Cheap, so it stays loaded while the preset module does not. */
775
+ interface PresetDetection {
776
+ /** Without `detect`, the preset applies only when listed in config. */
777
+ readonly detect?: ((ctx: DetectContext) => boolean | Promise<boolean>) | undefined;
778
+ /** When true and detected, presets that come after it are not applied. */
779
+ readonly exclusive?: boolean | undefined;
780
+ }
781
+ /**
782
+ * A built-in preset split in two: the detection is always loaded; the module behind `load`
783
+ * is imported only when it passes or the preset is listed in config.
784
+ */
785
+ interface PresetEntry extends PresetDetection {
786
+ readonly load: PresetLoader;
787
+ }
730
788
  //#endregion
731
789
  //#region src/types/config.d.ts
732
790
  /** Where a value comes from: an argument, or a property of an object argument. */
@@ -742,8 +800,8 @@ interface CallSpec {
742
800
  readonly kind?: "call" | "selector" | "accessor" | undefined;
743
801
  /** Which argument holds the key. Default 0. */
744
802
  readonly key?: number | ArgSource | undefined;
745
- /** Where the namespace comes from (call option, or factory argument when `factory` is true). */
746
- readonly namespace?: ArgSource | undefined;
803
+ /** Where the namespace comes from (call option, or factory argument when `factory` is true), or a fixed namespace such as `'shared'` for a hook that wraps `useTranslation('shared')`. */
804
+ readonly namespace?: ArgSource | string | undefined;
747
805
  /** Where a key prefix comes from (`keyPrefix` option, or next-intl `useTranslations('a.b')`). */
748
806
  readonly keyPrefix?: ArgSource | undefined;
749
807
  /** The callee is a factory that returns the translate function, for example `useI18n()` returning `{ t }`. */
@@ -835,6 +893,12 @@ interface NamespaceOptions {
835
893
  readonly separator?: string | undefined;
836
894
  /** Namespace of keys written without one. */
837
895
  readonly default?: string | undefined;
896
+ /** Namespaces searched in order when a key written without a namespace is not in its own (i18next `fallbackNS`). */
897
+ readonly fallback?: readonly string[] | undefined;
898
+ /** Namespace of keys written without one, per glob of source files, before `default`: `{ 'packages/admin/**': 'admin' }`. */
899
+ readonly byFile?: {
900
+ readonly [glob: string]: string;
901
+ } | undefined;
838
902
  /** How `{namespaces}` folders name a namespace: `dot` makes `pages/home` into `pages.home`, `slash` keeps `pages/home`, `camel` makes `todos-page` into `todosPage`. Default `dot`. */
839
903
  readonly fromPath?: "dot" | "slash" | "camel" | undefined;
840
904
  }
@@ -926,6 +990,10 @@ interface ResolvedConfig {
926
990
  readonly namespace: {
927
991
  readonly separator: string;
928
992
  readonly default: string | undefined;
993
+ readonly fallback: readonly string[];
994
+ readonly byFile: {
995
+ readonly [glob: string]: string;
996
+ };
929
997
  readonly fromPath: NamespaceFromPath;
930
998
  };
931
999
  readonly usage: {
@@ -984,6 +1052,34 @@ export declare function keyMatcher(globs: readonly string[]): (key: KeyString) =
984
1052
  /** Matcher over posix file paths. */
985
1053
  export declare function pathMatcher(globs: readonly string[]): (path: string) => boolean;
986
1054
  //#endregion
1055
+ //#region src/library.d.ts
1056
+ interface LibraryOptions {
1057
+ /** The plugin name, prefixed by the caller: `x18n:vue-i18n`. */
1058
+ readonly name: string;
1059
+ readonly calls?: readonly CallSpec[];
1060
+ /** i18n-ally style `usageMatchRegex` patterns, for languages without an AST scanner. */
1061
+ readonly regex?: readonly RegexScannerSpec[];
1062
+ /** The library's placeholder syntaxes. */
1063
+ readonly placeholders?: readonly PlaceholderSyntax[];
1064
+ readonly derivedKeys?: readonly string[];
1065
+ readonly namespace?: {
1066
+ readonly separator?: string;
1067
+ readonly default?: string;
1068
+ readonly fromPath?: NamespaceFromPath;
1069
+ };
1070
+ /** Default resource patterns (i18n-ally `preferredLocalePaths` + `pathMatcher`). */
1071
+ readonly resources?: readonly ResourceSpec[];
1072
+ readonly keyStyle?: "nested" | "flat";
1073
+ readonly templates?: readonly ExtractTemplate[];
1074
+ readonly finders?: readonly LocaleFinder[];
1075
+ readonly normalizeKey?: Plugin["normalizeKey"];
1076
+ readonly transformResource?: ResourceTransform;
1077
+ readonly extra?: (draft: ConfigDraft, ctx: ConfigContext) => void | Promise<void>;
1078
+ readonly translatePrompt?: Plugin["translatePrompt"];
1079
+ }
1080
+ /** One plugin holding a library's call specs, regexes, namespace rules, defaults and extraction templates. */
1081
+ export declare function createLibraryPlugin(options: LibraryOptions): Plugin;
1082
+ //#endregion
987
1083
  //#region src/locale.d.ts
988
1084
  /** Lenient display parse of a locale code (`zh_CN`, `pt-BR`, `sr-Latn-RS`). Never changes the code. */
989
1085
  export declare function parseLocaleInfo(code: LocaleCode): LocaleInfo;
@@ -1036,9 +1132,11 @@ interface CapabilityConflict {
1036
1132
  /** The capabilities of the final plugin list, indexed for the project engine. */
1037
1133
  interface PluginRegistry {
1038
1134
  readonly plugins: readonly Plugin[];
1039
- readonly formats: readonly Format[];
1135
+ /** Every format, loaded or not; use `formatFor` and `loadFormatFor` to run one. */
1136
+ readonly formats: readonly (Format | LazyFormat)[];
1040
1137
  readonly finders: readonly LocaleFinder[];
1041
- readonly scanners: readonly Scanner[];
1138
+ /** Every scanner, loaded or not. */
1139
+ readonly scanners: readonly (Scanner | LazyScanner)[];
1042
1140
  readonly checks: ReadonlyMap<string, Check>;
1043
1141
  readonly reporters: ReadonlyMap<string, Reporter>;
1044
1142
  readonly placeholders: readonly PlaceholderSyntax[];
@@ -1046,10 +1144,18 @@ interface PluginRegistry {
1046
1144
  readonly extractors: readonly Extractor[];
1047
1145
  readonly histories: readonly HistoryProvider[];
1048
1146
  readonly conflicts: readonly CapabilityConflict[];
1049
- /** Format for a file path: config `formats` first, then the first format that claims the extension. */
1147
+ /** The loaded format for a file path: config `formats` first, then the first format that claims the extension. */
1050
1148
  readonly formatFor: (path: string, hint?: string) => Format | undefined;
1149
+ /** The id of the format `formatFor` would pick, loaded or not. */
1150
+ readonly formatIdFor: (path: string, hint?: string) => string | undefined;
1151
+ /** The loaded format with this id. */
1051
1152
  readonly formatById: (id: string) => Format | undefined;
1052
- readonly scannersFor: (path: string) => readonly Scanner[];
1153
+ /** Loads the format of a file path, once, and returns it. */
1154
+ readonly loadFormatFor: (path: string, hint?: string) => Promise<Format | undefined>;
1155
+ /** The scanners that handle a file path, loaded or not. */
1156
+ readonly scannersFor: (path: string) => readonly (Scanner | LazyScanner)[];
1157
+ /** Loads the scanners of a file path, once each, and returns them. */
1158
+ readonly loadScannersFor: (path: string) => Promise<readonly Scanner[]>;
1053
1159
  }
1054
1160
  export declare function extensionOf(path: string): string;
1055
1161
  export declare function createRegistry(input: readonly Plugin[], config: Pick<ResolvedConfig, "formats">): PluginRegistry;
@@ -1116,7 +1222,7 @@ export declare function renderPrompt(data: PromptData): string;
1116
1222
  interface Builtins {
1117
1223
  readonly plugins: readonly Plugin[];
1118
1224
  readonly presets: {
1119
- readonly [name: string]: PresetFactory;
1225
+ readonly [name: string]: PresetEntry;
1120
1226
  };
1121
1227
  }
1122
1228
  interface ResolveConfigOptions {
@@ -1473,4 +1579,4 @@ export declare function rangeContains(range: TextRange, offset: number): boolean
1473
1579
  /** Fast non-cryptographic string hash (cyrb53), as hex. */
1474
1580
  export declare function hashText(text: string): string;
1475
1581
  //#endregion
1476
- 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, PluralForm, 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, X18nError };
1582
+ export type { Analysis, Answers, ApplyResult, ArgSource, Builtins, CacheStore, CallSpec, CapabilityConflict, Check, CheckContext, CheckIds, CheckLevel, CheckSetting, ConfigContext, ConfigDraft, ConfigOrigin, CreateProjectOptions, Decision, DecisionOption, DetectContext, Diagnostic, DiagnosticInput, Disposable, EditOp, Envelope, ErrorCode, ErrorInfo, ExtractItem, ExtractOptions, ExtractTemplate, Extractor, FileChange, FileEvent, FileScan, FinderContext, Fix, Format, FormatContext, FormatEntry, GlobOptions, GlossaryEntry, HardString, HistoryConfigs, HistoryContext, HistoryOptions, HistoryProvider, HistorySettings, Host, JsonObject, JsonPrimitive, JsonValue, KeyContext, KeyId, KeyInfo, KeyQuery, KeyString, KeyTarget, LazyFormat, LazyScanner, LibraryOptions, LineIndex, LinePosition, LocaleCode, LocaleCoverage, LocaleFinder, LocaleInfo, Location, Logger, ManifestReader, MemoryHost, Message, MessageChange, MessageHistory, MessageLeaf, MessageOp, MessageTree, NamespaceFromPath, NamespaceOptions, NodeRange, OverlayHost, ParseResult, Placeholder, PlaceholderComparer, PlaceholderProblem, PlaceholderSyntax, Plan, Plugin, PluginConfigs, PluginFile, PluginLoader, PluginOption, PluginRegistry, PluginSpecifier, PluralForm, Preset, PresetDetection, PresetEntry, PresetExplanation, PresetFactory, PresetLoader, PresetOption, Project, ProjectChange, ProjectExplanation, PromptContext, PromptData, PromptDraft, PromptItem, PromptOptions, RegexScannerSpec, ReportInput, ReportSummary, Reporter, ReporterContext, ReporterLoader, ResolveConfigOptions, ResolvedConfig, Resource, ResourceBlock, ResourceCandidate, ResourceId, ResourceNode, ResourceSpec, ResourceTransform, ResourceTransformContext, ScanContext, ScanResult, Scanner, ScannerEntry, SchemaIssue, Scope, SerializableConfig, Setup, Severity, SourceFile, TextEdit, TextRange, TreeLeaf, Usage, UsageCandidate, UsageContext, UsageKind, UsageMatch, UsageOptions, UsageQuery, UserConfig, WireDiagnostic, WireRange, WriteOptions, X18nError };
package/dist/index.mjs CHANGED
@@ -138,7 +138,9 @@ function defineConfig(config) {
138
138
  }
139
139
  //#endregion
140
140
  //#region src/errors.ts
141
- const at = (p) => p.line === void 0 ? p.path : `${p.path}:${p.line}`;
141
+ function at(p) {
142
+ return p.line === void 0 ? p.path : `${p.path}:${p.line}`;
143
+ }
142
144
  /**
143
145
  * Every coded error. Groups: 0xxx config, 1xxx plugin, 2xxx discovery and parse,
144
146
  * 3xxx scanning, 4xxx edits, 5xxx reviews. `fix` stays static so the docs can show it.
@@ -331,6 +333,40 @@ function pathMatcher(globs) {
331
333
  return (path) => regexes.some((re) => re.test(path));
332
334
  }
333
335
  //#endregion
336
+ //#region src/library.ts
337
+ /** One plugin holding a library's call specs, regexes, namespace rules, defaults and extraction templates. */
338
+ function createLibraryPlugin(options) {
339
+ return {
340
+ name: options.name,
341
+ async config(draft, ctx) {
342
+ if (options.namespace) draft.namespace = {
343
+ ...draft.namespace,
344
+ separator: options.namespace.separator ?? draft.namespace.separator,
345
+ default: draft.namespace.default ?? options.namespace.default,
346
+ fromPath: options.namespace.fromPath ?? draft.namespace.fromPath
347
+ };
348
+ if (options.resources) draft.resources = [...draft.resources, ...options.resources.filter((spec) => !draft.resources.some((r) => r.pattern === spec.pattern))];
349
+ if (options.keyStyle && draft.keyStyle === "auto" && !ctx.user.keyStyle) draft.keyStyle = options.keyStyle;
350
+ draft.usage = {
351
+ ...draft.usage,
352
+ calls: [...draft.usage.calls, ...options.calls ?? []],
353
+ regex: [...draft.usage.regex, ...options.regex ?? []],
354
+ derivedKeys: [...draft.usage.derivedKeys, ...options.derivedKeys ?? []]
355
+ };
356
+ await options.extra?.(draft, ctx);
357
+ },
358
+ placeholders: options.placeholders,
359
+ localeFinders: options.finders,
360
+ normalizeKey: options.normalizeKey,
361
+ transformResource: options.transformResource,
362
+ translatePrompt: options.translatePrompt,
363
+ extractors: options.templates ? [{
364
+ id: options.name,
365
+ templates: options.templates
366
+ }] : void 0
367
+ };
368
+ }
369
+ //#endregion
334
370
  //#region src/locale.ts
335
371
  const defaultRegions = {
336
372
  ar: "SA",
@@ -585,8 +621,8 @@ function uniqueById(items, kind) {
585
621
  }
586
622
  function createRegistry(input, config) {
587
623
  const plugins = sortByEnforce(input);
588
- const formats = plugins.flatMap((plugin) => plugin.formats ?? []);
589
- const scanners = plugins.flatMap((plugin) => plugin.scanners ?? []);
624
+ const formats = plugins.flatMap((plugin) => [...plugin.formats ?? [], ...plugin.lazyFormats ?? []]);
625
+ const scanners = plugins.flatMap((plugin) => [...plugin.scanners ?? [], ...plugin.lazyScanners ?? []]);
590
626
  const finders = [...uniqueById(plugins.flatMap((plugin) => plugin.localeFinders ?? []), "Locale finder").values()];
591
627
  const checks = uniqueById(plugins.flatMap((plugin) => plugin.checks ?? []), "Check");
592
628
  const reporters = uniqueById(plugins.flatMap((plugin) => plugin.reporters ?? []), "Reporter");
@@ -602,8 +638,58 @@ function createRegistry(input, config) {
602
638
  });
603
639
  else byExtension.set(extension, format);
604
640
  }
605
- const formatById = (id) => formats.find((format) => format.id === id);
641
+ const isLazy = (format) => "load" in format;
642
+ const loaded = new Map(formats.filter((format) => !isLazy(format)).map((format) => [format.id, format]));
643
+ const loading = /* @__PURE__ */ new Map();
644
+ const formatById = (id) => loaded.get(id);
645
+ const formatIdFor = (path, hint) => {
646
+ if (hint) return hint;
647
+ const extension = extensionOf(path);
648
+ return config.formats[extension] ?? byExtension.get(extension)?.id;
649
+ };
650
+ const loadFormat = (id) => {
651
+ const ready = loaded.get(id);
652
+ if (ready) return Promise.resolve(ready);
653
+ const lazy = formats.find((format) => format.id === id);
654
+ if (!lazy || !isLazy(lazy)) return Promise.resolve(void 0);
655
+ let pending = loading.get(id);
656
+ if (!pending) {
657
+ pending = lazy.load().then((mod) => {
658
+ const format = mod.default().formats?.find((item) => item.id === id && !isLazy(item));
659
+ if (format) loaded.set(id, format);
660
+ return format;
661
+ });
662
+ loading.set(id, pending);
663
+ }
664
+ return pending;
665
+ };
606
666
  const scannerCache = /* @__PURE__ */ new Map();
667
+ const loadedScanners = new Map(scanners.filter((scanner) => !("load" in scanner)).map((scanner) => [scanner.id, scanner]));
668
+ const loadingScanners = /* @__PURE__ */ new Map();
669
+ const loadScanner = (entry) => {
670
+ if (!("load" in entry)) return Promise.resolve(entry);
671
+ const ready = loadedScanners.get(entry.id);
672
+ if (ready) return Promise.resolve(ready);
673
+ let pending = loadingScanners.get(entry.id);
674
+ if (!pending) {
675
+ pending = entry.load().then((mod) => {
676
+ const scanner = mod.default().scanners?.find((item) => item.id === entry.id);
677
+ if (scanner) loadedScanners.set(entry.id, scanner);
678
+ return scanner;
679
+ });
680
+ loadingScanners.set(entry.id, pending);
681
+ }
682
+ return pending;
683
+ };
684
+ const scannersFor = (path) => {
685
+ const extension = extensionOf(path);
686
+ let list = scannerCache.get(extension);
687
+ if (!list) {
688
+ list = extension ? scanners.filter((scanner) => scanner.extensions.includes(extension) || scanner.extensions.includes("*")) : [];
689
+ scannerCache.set(extension, list);
690
+ }
691
+ return list;
692
+ };
607
693
  return {
608
694
  plugins,
609
695
  formats,
@@ -617,20 +703,18 @@ function createRegistry(input, config) {
617
703
  histories: plugins.flatMap((plugin) => plugin.histories ?? []),
618
704
  conflicts,
619
705
  formatById,
706
+ formatIdFor,
620
707
  formatFor(path, hint) {
621
- if (hint) return formatById(hint);
622
- const extension = extensionOf(path);
623
- const mapped = config.formats[extension];
624
- return mapped ? formatById(mapped) : byExtension.get(extension);
708
+ const id = formatIdFor(path, hint);
709
+ return id === void 0 ? void 0 : formatById(id);
625
710
  },
626
- scannersFor(path) {
627
- const extension = extensionOf(path);
628
- let list = scannerCache.get(extension);
629
- if (!list) {
630
- list = extension ? scanners.filter((scanner) => scanner.extensions.includes(extension) || scanner.extensions.includes("*")) : [];
631
- scannerCache.set(extension, list);
632
- }
633
- return list;
711
+ async loadFormatFor(path, hint) {
712
+ const id = formatIdFor(path, hint);
713
+ return id === void 0 ? void 0 : loadFormat(id);
714
+ },
715
+ scannersFor,
716
+ async loadScannersFor(path) {
717
+ return (await Promise.all(scannersFor(path).map(loadScanner))).filter((scanner) => scanner !== void 0);
634
718
  }
635
719
  };
636
720
  }
@@ -665,7 +749,7 @@ const callSpec = v.strictObject({
665
749
  "accessor"
666
750
  ])),
667
751
  key: v.optional(v.union([v.number(), argSource])),
668
- namespace: v.optional(argSource),
752
+ namespace: v.optional(v.union([argSource, v.string()])),
669
753
  keyPrefix: v.optional(argSource),
670
754
  factory: v.optional(v.boolean()),
671
755
  returns: v.optional(strings),
@@ -724,6 +808,8 @@ const checkSetting = v.union([level, v.tuple([level, jsonObject])]);
724
808
  const namespaceOptions = v.strictObject({
725
809
  separator: v.optional(v.string()),
726
810
  default: v.optional(v.string()),
811
+ fallback: v.optional(strings),
812
+ byFile: v.optional(v.record(v.string(), v.string())),
727
813
  fromPath: v.optional(v.picklist([
728
814
  "dot",
729
815
  "slash",
@@ -949,8 +1035,12 @@ function innermost(scopes, range) {
949
1035
  return best;
950
1036
  }
951
1037
  function createUsageResolver(config, namespaced, normalizers) {
952
- const separator = config.namespace.separator;
953
- return { resolve(candidate, scopes, path, scanner) {
1038
+ const { separator, fallback } = config.namespace;
1039
+ const byFile = Object.entries(config.namespace.byFile).map(([glob, namespace]) => ({
1040
+ matches: pathMatcher([glob]),
1041
+ namespace
1042
+ }));
1043
+ return { resolve(candidate, scopes, path, scanner, has) {
954
1044
  const scope = innermost(scopes, candidate.range);
955
1045
  const ctx = { path };
956
1046
  let text = candidate.raw;
@@ -961,8 +1051,8 @@ function createUsageResolver(config, namespaced, normalizers) {
961
1051
  break;
962
1052
  }
963
1053
  }
964
- const scopeNamespace = candidate.namespace ?? scope?.namespace;
965
- const scopePrefix = candidate.keyPrefix ?? scope?.keyPrefix;
1054
+ const scopeNamespace = candidate.namespace ?? scope?.namespace ?? byFile.find((rule) => rule.matches(path))?.namespace;
1055
+ let scopePrefix = candidate.keyPrefix ?? scope?.keyPrefix;
966
1056
  let namespace;
967
1057
  let explicit = false;
968
1058
  if (namespaced) {
@@ -973,10 +1063,17 @@ function createUsageResolver(config, namespaced, normalizers) {
973
1063
  text = text.slice(index + separator.length);
974
1064
  } else namespace = scopeNamespace ?? config.namespace.default;
975
1065
  }
976
- if (scopePrefix && !explicit) text = text ? `${scopePrefix}.${text}` : scopePrefix;
1066
+ if (scopePrefix && !explicit) {
1067
+ const prefixed = text ? `${scopePrefix}.${text}` : scopePrefix;
1068
+ const lookup = candidate.dynamic === "prefix" ? prefixed.replace(/\.$/, "") : prefixed;
1069
+ if (candidate.keyPrefix || !scope?.optionalPrefix || has(joinKey(namespace, lookup.split(".")))) text = prefixed;
1070
+ else scopePrefix = void 0;
1071
+ }
1072
+ const keyPath = text === "" ? [] : text.split(".");
977
1073
  let key;
978
1074
  if (candidate.dynamic === "unknown") key = void 0;
979
- else key = joinKey(namespace, text === "" ? [] : text.split("."));
1075
+ else key = joinKey(namespace, keyPath);
1076
+ if (key !== void 0 && namespaced && !explicit && candidate.dynamic === false && !has(key)) key = fallback.map((ns) => joinKey(ns, keyPath)).find(has) ?? key;
980
1077
  return {
981
1078
  key,
982
1079
  raw: candidate.raw,
@@ -1345,7 +1442,8 @@ function mirrorPath(path, from, to) {
1345
1442
  function importEdit(text, lines) {
1346
1443
  const missing = lines.filter((line) => !text.includes(line.trim()));
1347
1444
  if (missing.length === 0) return void 0;
1348
- const script = /<script\b[^>]*>\r?\n?/.exec(text);
1445
+ const open = /^\s*---\r?\n/.exec(text);
1446
+ const script = (open && /^---\s*$/m.test(text.slice(open[0].length)) ? open : void 0) ?? /<script\b[^>]*>\r?\n?/.exec(text);
1349
1447
  let at = 0;
1350
1448
  if (script) at = script.index + script[0].length;
1351
1449
  else {
@@ -1439,7 +1537,9 @@ var PlanBuilder = class {
1439
1537
  const sourceState = source && this.stateById.get(source.resource);
1440
1538
  const namespace = namespaced ? this.keyId(key).namespace ?? this.config.namespace.default : sourceState?.resource.namespace;
1441
1539
  const exists = (path) => resources.some((r) => r.resource.path === path);
1442
- const resolved = this.options.resolveNew(locale, namespace);
1540
+ const sibling = (r) => r.resource.locale === locale && r.resource.namespace !== void 0 && namespace !== void 0;
1541
+ const model = (sourceState && fits(sourceState) ? sourceState : void 0) ?? resources.find((s) => s.resource.locale === sourceLocale && fits(s)) ?? resources.find(fits) ?? resources.find(sibling);
1542
+ const resolved = this.options.resolveNew(locale, namespace, model?.resource);
1443
1543
  if (resolved && !exists(resolved)) {
1444
1544
  const format = this.options.registry.formatFor(resolved);
1445
1545
  if (format?.serialize) {
@@ -1455,8 +1555,6 @@ var PlanBuilder = class {
1455
1555
  return target;
1456
1556
  }
1457
1557
  }
1458
- const sibling = (r) => r.resource.locale === locale && r.resource.namespace !== void 0 && namespace !== void 0;
1459
- const model = (sourceState && fits(sourceState) ? sourceState : void 0) ?? resources.find((s) => s.resource.locale === sourceLocale && fits(s)) ?? resources.find(fits) ?? resources.find(sibling);
1460
1558
  const format = model && this.options.registry.formatById(model.resource.format);
1461
1559
  let path = model && mirrorPath(model.resource.path, model.resource.locale, locale);
1462
1560
  if (model && path && namespace !== void 0 && model.resource.namespace !== void 0 && model.resource.namespace !== namespace) path = mirrorPath(path, model.resource.namespace, namespace);
@@ -2265,6 +2363,22 @@ function createProject(options) {
2265
2363
  if (state.error) return void 0;
2266
2364
  return new Map([...walkLeaves(state.tree)].map((leaf) => [keyFor(state.resource, leaf.path), leaf.value]));
2267
2365
  }
2366
+ /** A path for a new resource, from the locale finders. */
2367
+ function newResourcePath(locale, namespace, model) {
2368
+ const ctx = {
2369
+ host,
2370
+ config,
2371
+ extensions: [...new Set(registry.formats.flatMap((format) => format.extensions))]
2372
+ };
2373
+ for (const finder of registry.finders) {
2374
+ const path = finder.resolveNew?.({
2375
+ locale,
2376
+ namespace,
2377
+ model
2378
+ }, ctx);
2379
+ if (path) return path;
2380
+ }
2381
+ }
2268
2382
  async function loadResources(candidates, reread) {
2269
2383
  const next = /* @__PURE__ */ new Map();
2270
2384
  const paths = [...new Set(candidates.map((c) => c.path))];
@@ -2276,6 +2390,7 @@ function createProject(options) {
2276
2390
  }
2277
2391
  }));
2278
2392
  namespaced = config.namespace.separator !== "." && candidates.some((c) => c.namespace !== void 0);
2393
+ await Promise.all(candidates.map((candidate) => registry.loadFormatFor(candidate.path, candidate.format)));
2279
2394
  for (const candidate of candidates) {
2280
2395
  const text = fileTexts.get(candidate.path);
2281
2396
  if (text === void 0) continue;
@@ -2296,6 +2411,11 @@ function createProject(options) {
2296
2411
  resolver = createUsageResolver(config, namespaced, normalizers);
2297
2412
  resolved = /* @__PURE__ */ new WeakMap();
2298
2413
  resourceIndex = void 0;
2414
+ const namespaces = /* @__PURE__ */ new Set([void 0, ...[...resourceStates.values()].map((state) => state.resource.namespace)]);
2415
+ await Promise.all([...namespaces].map((namespace) => {
2416
+ const path = newResourcePath(sourceLocale, namespace, void 0);
2417
+ return path ? registry.loadFormatFor(path) : void 0;
2418
+ }));
2299
2419
  }
2300
2420
  async function scanOne(file) {
2301
2421
  const usages = [];
@@ -2303,7 +2423,7 @@ function createProject(options) {
2303
2423
  const keySlots = [];
2304
2424
  const hardStrings = [];
2305
2425
  const calls = config.usage.calls.filter((spec) => ![spec.callee].flat().some((name) => config.usage.ignoreCallees.includes(name)));
2306
- for (const scanner of registry.scannersFor(file.path)) {
2426
+ for (const scanner of await registry.loadScannersFor(file.path)) {
2307
2427
  const result = await scanner.scan(file, {
2308
2428
  config,
2309
2429
  calls
@@ -2470,12 +2590,13 @@ function createProject(options) {
2470
2590
  };
2471
2591
  }
2472
2592
  function buildIndex() {
2473
- resourceIndex ??= buildResourceIndex();
2593
+ const { messages, parents, derived } = resourceIndex ??= buildResourceIndex();
2594
+ const has = (key) => messages.has(key) || parents.has(key) || derived.has(key);
2474
2595
  const usages = [];
2475
2596
  for (const [path, state] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
2476
2597
  let list = resolved.get(state);
2477
2598
  if (!list) {
2478
- list = state.result.usages.map((candidate) => resolver.resolve(candidate, state.result.scopes ?? [], path, "scanner"));
2599
+ list = state.result.usages.map((candidate) => resolver.resolve(candidate, state.result.scopes ?? [], path, "scanner", has));
2479
2600
  resolved.set(state, list);
2480
2601
  }
2481
2602
  usages.push(...list);
@@ -2708,8 +2829,9 @@ function createProject(options) {
2708
2829
  const before = idx();
2709
2830
  const paths = new Set(events.map((event) => event.path));
2710
2831
  const resourcePaths = new Set([...resourceStates.values()].map((state) => state.resource.path));
2711
- const maybeResource = (path) => !resourcePaths.has(path) && registry.formatFor(path) !== void 0 && !usageEligible(path);
2712
- const structural = events.some((event) => event.type !== "change" || maybeResource(event.path));
2832
+ const maybeResource = (path) => !resourcePaths.has(path) && registry.formatIdFor(path) !== void 0 && !usageEligible(path);
2833
+ const blockPaths = new Set([...resourceStates.values()].filter((state) => state.resource.block).map((state) => state.resource.path));
2834
+ const structural = events.some((event) => event.type !== "change" || maybeResource(event.path) || blockPaths.has(event.path));
2713
2835
  if (structural || [...paths].some((path) => resourcePaths.has(path))) await loadResources(structural ? await discover() : [...resourceStates.values()].map((state) => ({
2714
2836
  ...state.resource,
2715
2837
  format: state.resource.format === "unknown" ? void 0 : state.resource.format
@@ -2750,20 +2872,7 @@ function createProject(options) {
2750
2872
  text: (path) => fileTexts.get(path) ?? files.get(path)?.text,
2751
2873
  basesOf,
2752
2874
  toStored: (target, path) => storedPathOf(target)(path),
2753
- resolveNew(locale, namespace) {
2754
- const ctx = {
2755
- host,
2756
- config,
2757
- extensions: [...new Set(registry.formats.flatMap((format) => format.extensions))]
2758
- };
2759
- for (const finder of registry.finders) {
2760
- const path = finder.resolveNew?.({
2761
- locale,
2762
- namespace
2763
- }, ctx);
2764
- if (path) return path;
2765
- }
2766
- },
2875
+ resolveNew: newResourcePath,
2767
2876
  update
2768
2877
  });
2769
2878
  const ready = (async () => {
@@ -2894,6 +3003,8 @@ function defaultDraft() {
2894
3003
  namespace: {
2895
3004
  separator: ".",
2896
3005
  default: void 0,
3006
+ fallback: [],
3007
+ byFile: {},
2897
3008
  fromPath: "dot"
2898
3009
  },
2899
3010
  usage: {
@@ -2977,6 +3088,11 @@ function applyLayer(draft, layer, origin, origins) {
2977
3088
  draft.namespace = {
2978
3089
  separator: layer.namespace.separator ?? draft.namespace.separator,
2979
3090
  default: layer.namespace.default ?? draft.namespace.default,
3091
+ fallback: layer.namespace.fallback ?? draft.namespace.fallback,
3092
+ byFile: {
3093
+ ...draft.namespace.byFile,
3094
+ ...layer.namespace.byFile
3095
+ },
2980
3096
  fromPath: layer.namespace.fromPath ?? draft.namespace.fromPath
2981
3097
  };
2982
3098
  mark("namespace");
@@ -3081,8 +3197,8 @@ function createLoader(options) {
3081
3197
  if (option === false || option === void 0) return void 0;
3082
3198
  if (typeof option === "object" && !Array.isArray(option)) return option;
3083
3199
  const [specifier, pluginOptions] = typeof option === "string" ? [option, void 0] : [option[0], option[1]];
3084
- const factory = options.builtins.presets[specifier];
3085
- if (factory) return factory(pluginOptions);
3200
+ const entry = options.builtins.presets[specifier];
3201
+ if (entry) return (await entry.load()).default(pluginOptions);
3086
3202
  if (!options.loadPlugin) throw errors.X18N1003({ preset: specifier });
3087
3203
  return options.loadPlugin(specifier, pluginOptions);
3088
3204
  };
@@ -3118,7 +3234,17 @@ async function expandPresets(presets, userPlugins, listed, detectCtx) {
3118
3234
  name: preset.name,
3119
3235
  reason: "after-exclusive"
3120
3236
  });
3121
- else stopped = await flatten(preset, listed);
3237
+ else if ("entry" in preset) {
3238
+ const applies = listed || (preset.entry.detect ? await preset.entry.detect(detectCtx) : false);
3239
+ explanations.push({
3240
+ name: preset.name,
3241
+ reason: applies ? listed ? "listed" : "detected" : "not-detected"
3242
+ });
3243
+ if (!applies) continue;
3244
+ const loaded = (await preset.entry.load()).default();
3245
+ for (const child of loaded.plugins) await flatten(child, false);
3246
+ stopped = preset.entry.exclusive === true || loaded.exclusive === true;
3247
+ } else stopped = await flatten(preset, listed);
3122
3248
  for (const plugin of userPlugins) await flatten(plugin, true);
3123
3249
  return {
3124
3250
  plugins,
@@ -3141,7 +3267,10 @@ async function resolveConfig(options) {
3141
3267
  const userPlugins = await loadAll(load, [...options.parent?.plugins ?? [], ...user.plugins ?? []]);
3142
3268
  const presetOption = user.presets ?? options.parent?.presets ?? "auto";
3143
3269
  const auto = presetOption === "auto";
3144
- const presets = auto ? Object.values(builtins.presets).map((factory) => factory()) : (await loadAll(load, presetOption)).map((item) => isPreset(item) ? item : {
3270
+ const presets = auto ? Object.entries(builtins.presets).map(([name, entry]) => ({
3271
+ name,
3272
+ entry
3273
+ })) : (await loadAll(load, presetOption)).map((item) => isPreset(item) ? item : {
3145
3274
  name: item.name,
3146
3275
  plugins: [item]
3147
3276
  });
@@ -3182,4 +3311,4 @@ async function resolveConfig(options) {
3182
3311
  };
3183
3312
  }
3184
3313
  //#endregion
3185
- export { ConfigSchema, PluginFileSchema, applyMessageOps, applyTextEdits, buildTree, contractVersion, createLineIndex, createMemoryHost, createOverlayHost, createPlaceholderComparer, createProject, createRegistry, defineConfig, definePlugin, definePreset, diffText, envelope, errors, extensionOf, globToRegExp, hashText, isJsonObject, isKeyUnder, isMessageLeaf, isMessageTree, isPreset, isX18nError, joinKey, keyMatcher, localeSpellingKey, parseLocaleInfo, parseMessageBatch, pathId, pathMatcher, placeholderProblems, rangeContains, renderPrompt, replaceKeyPrefix, resolveConfig, sortTree, splitKey, toErrorInfo, toWireDiagnostic, toWireRange, validate, walkLeaves };
3314
+ export { ConfigSchema, PluginFileSchema, applyMessageOps, applyTextEdits, buildTree, contractVersion, createLibraryPlugin, createLineIndex, createMemoryHost, createOverlayHost, createPlaceholderComparer, createProject, createRegistry, defineConfig, definePlugin, definePreset, diffText, envelope, errors, extensionOf, globToRegExp, hashText, isJsonObject, isKeyUnder, isMessageLeaf, isMessageTree, isPreset, isX18nError, joinKey, keyMatcher, localeSpellingKey, parseLocaleInfo, parseMessageBatch, pathId, pathMatcher, placeholderProblems, rangeContains, renderPrompt, replaceKeyPrefix, resolveConfig, sortTree, splitKey, toErrorInfo, toWireDiagnostic, toWireRange, validate, walkLeaves };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@x18n/core",
3
3
  "type": "module",
4
- "version": "0.0.13",
4
+ "version": "0.0.14",
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",
@@ -145,6 +145,21 @@
145
145
  "default": {
146
146
  "type": "string"
147
147
  },
148
+ "fallback": {
149
+ "type": "array",
150
+ "items": {
151
+ "type": "string"
152
+ }
153
+ },
154
+ "byFile": {
155
+ "type": "object",
156
+ "propertyNames": {
157
+ "type": "string"
158
+ },
159
+ "additionalProperties": {
160
+ "type": "string"
161
+ }
162
+ },
148
163
  "fromPath": {
149
164
  "enum": [
150
165
  "dot",
@@ -221,19 +236,26 @@
221
236
  ]
222
237
  },
223
238
  "namespace": {
224
- "type": "object",
225
- "properties": {
226
- "arg": {
227
- "type": "number"
239
+ "anyOf": [
240
+ {
241
+ "type": "object",
242
+ "properties": {
243
+ "arg": {
244
+ "type": "number"
245
+ },
246
+ "property": {
247
+ "type": "string"
248
+ }
249
+ },
250
+ "required": [
251
+ "arg"
252
+ ],
253
+ "additionalProperties": false
228
254
  },
229
- "property": {
255
+ {
230
256
  "type": "string"
231
257
  }
232
- },
233
- "required": [
234
- "arg"
235
- ],
236
- "additionalProperties": false
258
+ ]
237
259
  },
238
260
  "keyPrefix": {
239
261
  "type": "object",
@@ -90,6 +90,21 @@
90
90
  "default": {
91
91
  "type": "string"
92
92
  },
93
+ "fallback": {
94
+ "type": "array",
95
+ "items": {
96
+ "type": "string"
97
+ }
98
+ },
99
+ "byFile": {
100
+ "type": "object",
101
+ "propertyNames": {
102
+ "type": "string"
103
+ },
104
+ "additionalProperties": {
105
+ "type": "string"
106
+ }
107
+ },
93
108
  "fromPath": {
94
109
  "enum": [
95
110
  "dot",
@@ -166,19 +181,26 @@
166
181
  ]
167
182
  },
168
183
  "namespace": {
169
- "type": "object",
170
- "properties": {
171
- "arg": {
172
- "type": "number"
184
+ "anyOf": [
185
+ {
186
+ "type": "object",
187
+ "properties": {
188
+ "arg": {
189
+ "type": "number"
190
+ },
191
+ "property": {
192
+ "type": "string"
193
+ }
194
+ },
195
+ "required": [
196
+ "arg"
197
+ ],
198
+ "additionalProperties": false
173
199
  },
174
- "property": {
200
+ {
175
201
  "type": "string"
176
202
  }
177
- },
178
- "required": [
179
- "arg"
180
- ],
181
- "additionalProperties": false
203
+ ]
182
204
  },
183
205
  "keyPrefix": {
184
206
  "type": "object",