@x18n/core 0.0.12 → 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 +135 -12
- package/dist/index.mjs +361 -158
- package/package.json +1 -1
- package/schema/config.json +38 -10
- package/schema/plugin.json +32 -10
package/dist/index.d.mts
CHANGED
|
@@ -196,6 +196,8 @@ interface LocaleInfo {
|
|
|
196
196
|
readonly displayName: string | undefined;
|
|
197
197
|
readonly flag: string | undefined;
|
|
198
198
|
readonly pluralCategories: readonly Intl.LDMLPluralRule[];
|
|
199
|
+
/** Categories of ordinal numbers (1st, 2nd), which differ from the cardinal ones. */
|
|
200
|
+
readonly ordinalCategories: readonly Intl.LDMLPluralRule[];
|
|
199
201
|
}
|
|
200
202
|
/** Embedded resource inside a file, for example a Vue `<i18n>` block. */
|
|
201
203
|
interface ResourceBlock {
|
|
@@ -237,6 +239,8 @@ interface Usage {
|
|
|
237
239
|
readonly path: string;
|
|
238
240
|
/** Range of the key text only (inside the quotes): the range rename rewrites. */
|
|
239
241
|
readonly range: TextRange;
|
|
242
|
+
/** Range of the whole call that holds the key (`t('a', { n })`), when the scanner knows it. */
|
|
243
|
+
readonly expression: TextRange | undefined;
|
|
240
244
|
readonly scanner: string;
|
|
241
245
|
readonly kind: UsageKind;
|
|
242
246
|
/** `t(`pages.${id}.title`)` gives 'prefix' with key `pages`. */
|
|
@@ -430,10 +434,14 @@ interface LocaleFinder {
|
|
|
430
434
|
readonly id: string;
|
|
431
435
|
readonly override?: boolean | undefined;
|
|
432
436
|
readonly find: (ctx: FinderContext) => Promise<readonly ResourceCandidate[]>;
|
|
433
|
-
/**
|
|
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
|
+
*/
|
|
434
441
|
readonly resolveNew?: ((target: {
|
|
435
442
|
readonly locale: LocaleCode;
|
|
436
443
|
readonly namespace: string | undefined;
|
|
444
|
+
readonly model: Resource | undefined;
|
|
437
445
|
}, ctx: FinderContext) => string | undefined) | undefined;
|
|
438
446
|
}
|
|
439
447
|
interface FormatContext {
|
|
@@ -455,6 +463,28 @@ type MessageOp = {
|
|
|
455
463
|
readonly type: "delete";
|
|
456
464
|
readonly path: readonly string[];
|
|
457
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
|
+
}
|
|
458
488
|
interface Format {
|
|
459
489
|
readonly id: string;
|
|
460
490
|
/** File extensions without the dot. */
|
|
@@ -479,6 +509,8 @@ interface ScanContext {
|
|
|
479
509
|
interface UsageCandidate {
|
|
480
510
|
readonly raw: string;
|
|
481
511
|
readonly range: TextRange;
|
|
512
|
+
/** Range of the whole call that holds the key, when the scanner knows it. */
|
|
513
|
+
readonly expression?: TextRange | undefined;
|
|
482
514
|
readonly kind: UsageKind;
|
|
483
515
|
readonly dynamic: false | "prefix" | "unknown";
|
|
484
516
|
readonly namespace?: string | undefined;
|
|
@@ -490,6 +522,8 @@ interface Scope {
|
|
|
490
522
|
readonly range: TextRange;
|
|
491
523
|
readonly namespace?: string | undefined;
|
|
492
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;
|
|
493
527
|
}
|
|
494
528
|
interface HardString {
|
|
495
529
|
readonly text: string;
|
|
@@ -571,6 +605,13 @@ interface HistoryProvider {
|
|
|
571
605
|
/** Calls `onChange` when the history changes outside resource files, for example on a commit. */
|
|
572
606
|
readonly watch?: ((ctx: HistoryContext, onChange: () => void) => Disposable) | undefined;
|
|
573
607
|
}
|
|
608
|
+
/** A key that is one plural form of a base key, such as `item_one` of `item`. */
|
|
609
|
+
interface PluralForm {
|
|
610
|
+
readonly type: Intl.PluralRuleType;
|
|
611
|
+
readonly category: Intl.LDMLPluralRule;
|
|
612
|
+
/** The sibling key of another category. */
|
|
613
|
+
readonly keyOf: (category: Intl.LDMLPluralRule) => KeyString;
|
|
614
|
+
}
|
|
574
615
|
interface CheckContext {
|
|
575
616
|
readonly config: ResolvedConfig;
|
|
576
617
|
readonly sourceLocale: LocaleCode;
|
|
@@ -587,6 +628,8 @@ interface CheckContext {
|
|
|
587
628
|
readonly message: (key: KeyString, locale: LocaleCode) => Message | undefined;
|
|
588
629
|
/** Every resource message of a key in a locale, more than one when duplicated across files. */
|
|
589
630
|
readonly duplicates: (key: KeyString, locale: LocaleCode) => readonly Message[];
|
|
631
|
+
/** The plural form a key is, from the `{plural}` and `{ordinal}` derived-key rules. */
|
|
632
|
+
readonly formOf: (key: KeyString) => PluralForm | undefined;
|
|
590
633
|
readonly usages: () => Iterable<Usage>;
|
|
591
634
|
readonly coverage: () => readonly LocaleCoverage[];
|
|
592
635
|
readonly unused: () => readonly KeyString[];
|
|
@@ -688,6 +731,10 @@ interface Plugin {
|
|
|
688
731
|
readonly manifests?: readonly ManifestReader[] | undefined;
|
|
689
732
|
readonly localeFinders?: readonly LocaleFinder[] | undefined;
|
|
690
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;
|
|
691
738
|
readonly scanners?: readonly Scanner[] | undefined;
|
|
692
739
|
readonly extractors?: readonly Extractor[] | undefined;
|
|
693
740
|
readonly checks?: readonly Check[] | undefined;
|
|
@@ -712,6 +759,32 @@ interface Preset {
|
|
|
712
759
|
}
|
|
713
760
|
/** A preset factory as referenced by name from YAML and JSON configs. */
|
|
714
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
|
+
}
|
|
715
788
|
//#endregion
|
|
716
789
|
//#region src/types/config.d.ts
|
|
717
790
|
/** Where a value comes from: an argument, or a property of an object argument. */
|
|
@@ -727,8 +800,8 @@ interface CallSpec {
|
|
|
727
800
|
readonly kind?: "call" | "selector" | "accessor" | undefined;
|
|
728
801
|
/** Which argument holds the key. Default 0. */
|
|
729
802
|
readonly key?: number | ArgSource | undefined;
|
|
730
|
-
/** Where the namespace comes from (call option, or factory argument when `factory` is true)
|
|
731
|
-
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;
|
|
732
805
|
/** Where a key prefix comes from (`keyPrefix` option, or next-intl `useTranslations('a.b')`). */
|
|
733
806
|
readonly keyPrefix?: ArgSource | undefined;
|
|
734
807
|
/** The callee is a factory that returns the translate function, for example `useI18n()` returning `{ t }`. */
|
|
@@ -785,12 +858,14 @@ interface ExtractOptions {
|
|
|
785
858
|
readonly keyMaxLength?: number | undefined;
|
|
786
859
|
/** Reuse a key when the same text exists as a source value. Default true. */
|
|
787
860
|
readonly reuseExisting?: boolean | undefined;
|
|
788
|
-
/** Strings or `/regex
|
|
861
|
+
/** Strings never extracted: an exact string such as `Elk`, or a `/regex/flags` string such as `/^v\d+/i` that matches the text. */
|
|
789
862
|
readonly ignore?: readonly string[] | undefined;
|
|
790
|
-
/**
|
|
863
|
+
/** Strings never extracted in the files of a glob, written as in `ignore`. */
|
|
791
864
|
readonly ignoreByFile?: {
|
|
792
865
|
readonly [glob: string]: readonly string[];
|
|
793
866
|
} | undefined;
|
|
867
|
+
/** Attribute names whose values are text, on top of those whose last word is `title`, `label`, `text`, `placeholder`, `alt`, `content`, `description`, `message` and the like. Markup attributes outside this set are never extracted. */
|
|
868
|
+
readonly attributes?: readonly string[] | undefined;
|
|
794
869
|
readonly ignoreAttributes?: readonly string[] | undefined;
|
|
795
870
|
readonly ignoreCallees?: readonly string[] | undefined;
|
|
796
871
|
readonly templates?: readonly (string | ExtractTemplate)[] | undefined;
|
|
@@ -804,7 +879,7 @@ interface UsageOptions {
|
|
|
804
879
|
readonly ignoreCallees?: readonly string[] | undefined;
|
|
805
880
|
/** Keys (globs) that count as used, for example keys built at runtime. */
|
|
806
881
|
readonly keysInUse?: readonly string[] | undefined;
|
|
807
|
-
/** Rules like `{key}_plural`: when `{key}` is used the derived keys count as used. */
|
|
882
|
+
/** Rules like `{key}_plural`: when `{key}` is used the derived keys count as used. Rules apply repeatedly, so with `{key}_*` the key `a_b_c` derives from both `a_b` and `a`. `*` matches one key segment. `{plural}` and `{ordinal}` match a plural or ordinal category (`one`, `few`, `other`): each locale then needs the categories of its own language, and categories the language lacks are optional. */
|
|
808
883
|
readonly derivedKeys?: readonly string[] | undefined;
|
|
809
884
|
}
|
|
810
885
|
interface WriteOptions {
|
|
@@ -818,6 +893,12 @@ interface NamespaceOptions {
|
|
|
818
893
|
readonly separator?: string | undefined;
|
|
819
894
|
/** Namespace of keys written without one. */
|
|
820
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;
|
|
821
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`. */
|
|
822
903
|
readonly fromPath?: "dot" | "slash" | "camel" | undefined;
|
|
823
904
|
}
|
|
@@ -909,6 +990,10 @@ interface ResolvedConfig {
|
|
|
909
990
|
readonly namespace: {
|
|
910
991
|
readonly separator: string;
|
|
911
992
|
readonly default: string | undefined;
|
|
993
|
+
readonly fallback: readonly string[];
|
|
994
|
+
readonly byFile: {
|
|
995
|
+
readonly [glob: string]: string;
|
|
996
|
+
};
|
|
912
997
|
readonly fromPath: NamespaceFromPath;
|
|
913
998
|
};
|
|
914
999
|
readonly usage: {
|
|
@@ -967,6 +1052,34 @@ export declare function keyMatcher(globs: readonly string[]): (key: KeyString) =
|
|
|
967
1052
|
/** Matcher over posix file paths. */
|
|
968
1053
|
export declare function pathMatcher(globs: readonly string[]): (path: string) => boolean;
|
|
969
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
|
|
970
1083
|
//#region src/locale.d.ts
|
|
971
1084
|
/** Lenient display parse of a locale code (`zh_CN`, `pt-BR`, `sr-Latn-RS`). Never changes the code. */
|
|
972
1085
|
export declare function parseLocaleInfo(code: LocaleCode): LocaleInfo;
|
|
@@ -1019,9 +1132,11 @@ interface CapabilityConflict {
|
|
|
1019
1132
|
/** The capabilities of the final plugin list, indexed for the project engine. */
|
|
1020
1133
|
interface PluginRegistry {
|
|
1021
1134
|
readonly plugins: readonly Plugin[];
|
|
1022
|
-
|
|
1135
|
+
/** Every format, loaded or not; use `formatFor` and `loadFormatFor` to run one. */
|
|
1136
|
+
readonly formats: readonly (Format | LazyFormat)[];
|
|
1023
1137
|
readonly finders: readonly LocaleFinder[];
|
|
1024
|
-
|
|
1138
|
+
/** Every scanner, loaded or not. */
|
|
1139
|
+
readonly scanners: readonly (Scanner | LazyScanner)[];
|
|
1025
1140
|
readonly checks: ReadonlyMap<string, Check>;
|
|
1026
1141
|
readonly reporters: ReadonlyMap<string, Reporter>;
|
|
1027
1142
|
readonly placeholders: readonly PlaceholderSyntax[];
|
|
@@ -1029,10 +1144,18 @@ interface PluginRegistry {
|
|
|
1029
1144
|
readonly extractors: readonly Extractor[];
|
|
1030
1145
|
readonly histories: readonly HistoryProvider[];
|
|
1031
1146
|
readonly conflicts: readonly CapabilityConflict[];
|
|
1032
|
-
/**
|
|
1147
|
+
/** The loaded format for a file path: config `formats` first, then the first format that claims the extension. */
|
|
1033
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. */
|
|
1034
1152
|
readonly formatById: (id: string) => Format | undefined;
|
|
1035
|
-
|
|
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[]>;
|
|
1036
1159
|
}
|
|
1037
1160
|
export declare function extensionOf(path: string): string;
|
|
1038
1161
|
export declare function createRegistry(input: readonly Plugin[], config: Pick<ResolvedConfig, "formats">): PluginRegistry;
|
|
@@ -1099,7 +1222,7 @@ export declare function renderPrompt(data: PromptData): string;
|
|
|
1099
1222
|
interface Builtins {
|
|
1100
1223
|
readonly plugins: readonly Plugin[];
|
|
1101
1224
|
readonly presets: {
|
|
1102
|
-
readonly [name: string]:
|
|
1225
|
+
readonly [name: string]: PresetEntry;
|
|
1103
1226
|
};
|
|
1104
1227
|
}
|
|
1105
1228
|
interface ResolveConfigOptions {
|
|
@@ -1456,4 +1579,4 @@ export declare function rangeContains(range: TextRange, offset: number): boolean
|
|
|
1456
1579
|
/** Fast non-cryptographic string hash (cyrb53), as hex. */
|
|
1457
1580
|
export declare function hashText(text: string): string;
|
|
1458
1581
|
//#endregion
|
|
1459
|
-
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, 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
|
-
|
|
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",
|
|
@@ -418,8 +454,10 @@ function parseLocaleInfo(code) {
|
|
|
418
454
|
region
|
|
419
455
|
].filter(Boolean).join("-");
|
|
420
456
|
const displayName = language ? safe(() => new Intl.DisplayNames(["en"], { type: "language" }).of(tag)) : void 0;
|
|
421
|
-
const
|
|
422
|
-
|
|
457
|
+
const categories = (type) => {
|
|
458
|
+
const found = language ? safe(() => new Intl.PluralRules(tag, { type }).resolvedOptions().pluralCategories) : void 0;
|
|
459
|
+
return found ? pluralOrder.filter((category) => found.includes(category)) : ["other"];
|
|
460
|
+
};
|
|
423
461
|
const flagRegion = region ?? (language === "zh" && script === "Hant" ? "TW" : language ? defaultRegions[language] : void 0);
|
|
424
462
|
return {
|
|
425
463
|
code,
|
|
@@ -427,7 +465,8 @@ function parseLocaleInfo(code) {
|
|
|
427
465
|
region,
|
|
428
466
|
displayName,
|
|
429
467
|
flag: flagRegion ? flagOf(flagRegion) : void 0,
|
|
430
|
-
pluralCategories
|
|
468
|
+
pluralCategories: categories("cardinal"),
|
|
469
|
+
ordinalCategories: categories("ordinal")
|
|
431
470
|
};
|
|
432
471
|
}
|
|
433
472
|
/** Codes that differ only in spelling (`zh_CN` and `zh-cn`), the same after a lenient normalization. */
|
|
@@ -582,8 +621,8 @@ function uniqueById(items, kind) {
|
|
|
582
621
|
}
|
|
583
622
|
function createRegistry(input, config) {
|
|
584
623
|
const plugins = sortByEnforce(input);
|
|
585
|
-
const formats = plugins.flatMap((plugin) => plugin.formats ?? []);
|
|
586
|
-
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 ?? []]);
|
|
587
626
|
const finders = [...uniqueById(plugins.flatMap((plugin) => plugin.localeFinders ?? []), "Locale finder").values()];
|
|
588
627
|
const checks = uniqueById(plugins.flatMap((plugin) => plugin.checks ?? []), "Check");
|
|
589
628
|
const reporters = uniqueById(plugins.flatMap((plugin) => plugin.reporters ?? []), "Reporter");
|
|
@@ -599,8 +638,58 @@ function createRegistry(input, config) {
|
|
|
599
638
|
});
|
|
600
639
|
else byExtension.set(extension, format);
|
|
601
640
|
}
|
|
602
|
-
const
|
|
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
|
+
};
|
|
603
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
|
+
};
|
|
604
693
|
return {
|
|
605
694
|
plugins,
|
|
606
695
|
formats,
|
|
@@ -614,20 +703,18 @@ function createRegistry(input, config) {
|
|
|
614
703
|
histories: plugins.flatMap((plugin) => plugin.histories ?? []),
|
|
615
704
|
conflicts,
|
|
616
705
|
formatById,
|
|
706
|
+
formatIdFor,
|
|
617
707
|
formatFor(path, hint) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const mapped = config.formats[extension];
|
|
621
|
-
return mapped ? formatById(mapped) : byExtension.get(extension);
|
|
708
|
+
const id = formatIdFor(path, hint);
|
|
709
|
+
return id === void 0 ? void 0 : formatById(id);
|
|
622
710
|
},
|
|
623
|
-
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
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);
|
|
631
718
|
}
|
|
632
719
|
};
|
|
633
720
|
}
|
|
@@ -662,7 +749,7 @@ const callSpec = v.strictObject({
|
|
|
662
749
|
"accessor"
|
|
663
750
|
])),
|
|
664
751
|
key: v.optional(v.union([v.number(), argSource])),
|
|
665
|
-
namespace: v.optional(argSource),
|
|
752
|
+
namespace: v.optional(v.union([argSource, v.string()])),
|
|
666
753
|
keyPrefix: v.optional(argSource),
|
|
667
754
|
factory: v.optional(v.boolean()),
|
|
668
755
|
returns: v.optional(strings),
|
|
@@ -706,6 +793,7 @@ const extractOptions = v.strictObject({
|
|
|
706
793
|
reuseExisting: v.optional(v.boolean()),
|
|
707
794
|
ignore: v.optional(strings),
|
|
708
795
|
ignoreByFile: v.optional(v.record(v.string(), strings)),
|
|
796
|
+
attributes: v.optional(strings),
|
|
709
797
|
ignoreAttributes: v.optional(strings),
|
|
710
798
|
ignoreCallees: v.optional(strings),
|
|
711
799
|
templates: v.optional(extractTemplates)
|
|
@@ -720,6 +808,8 @@ const checkSetting = v.union([level, v.tuple([level, jsonObject])]);
|
|
|
720
808
|
const namespaceOptions = v.strictObject({
|
|
721
809
|
separator: v.optional(v.string()),
|
|
722
810
|
default: v.optional(v.string()),
|
|
811
|
+
fallback: v.optional(strings),
|
|
812
|
+
byFile: v.optional(v.record(v.string(), v.string())),
|
|
723
813
|
fromPath: v.optional(v.picklist([
|
|
724
814
|
"dot",
|
|
725
815
|
"slash",
|
|
@@ -938,6 +1028,159 @@ function createHistory(input) {
|
|
|
938
1028
|
};
|
|
939
1029
|
}
|
|
940
1030
|
//#endregion
|
|
1031
|
+
//#region src/project/usages.ts
|
|
1032
|
+
function innermost(scopes, range) {
|
|
1033
|
+
let best;
|
|
1034
|
+
for (const scope of scopes) if (scope.range.start <= range.start && scope.range.end >= range.end && (!best || scope.range.end - scope.range.start < best.range.end - best.range.start)) best = scope;
|
|
1035
|
+
return best;
|
|
1036
|
+
}
|
|
1037
|
+
function createUsageResolver(config, namespaced, normalizers) {
|
|
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) {
|
|
1044
|
+
const scope = innermost(scopes, candidate.range);
|
|
1045
|
+
const ctx = { path };
|
|
1046
|
+
let text = candidate.raw;
|
|
1047
|
+
for (const normalize of normalizers) {
|
|
1048
|
+
const result = normalize(text, ctx);
|
|
1049
|
+
if (result !== void 0) {
|
|
1050
|
+
text = result;
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
const scopeNamespace = candidate.namespace ?? scope?.namespace ?? byFile.find((rule) => rule.matches(path))?.namespace;
|
|
1055
|
+
let scopePrefix = candidate.keyPrefix ?? scope?.keyPrefix;
|
|
1056
|
+
let namespace;
|
|
1057
|
+
let explicit = false;
|
|
1058
|
+
if (namespaced) {
|
|
1059
|
+
const index = text.indexOf(separator);
|
|
1060
|
+
explicit = index > 0;
|
|
1061
|
+
if (explicit) {
|
|
1062
|
+
namespace = text.slice(0, index);
|
|
1063
|
+
text = text.slice(index + separator.length);
|
|
1064
|
+
} else namespace = scopeNamespace ?? config.namespace.default;
|
|
1065
|
+
}
|
|
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(".");
|
|
1073
|
+
let key;
|
|
1074
|
+
if (candidate.dynamic === "unknown") key = void 0;
|
|
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;
|
|
1077
|
+
return {
|
|
1078
|
+
key,
|
|
1079
|
+
raw: candidate.raw,
|
|
1080
|
+
path,
|
|
1081
|
+
range: candidate.range,
|
|
1082
|
+
expression: candidate.expression,
|
|
1083
|
+
scanner,
|
|
1084
|
+
kind: candidate.kind,
|
|
1085
|
+
dynamic: candidate.dynamic,
|
|
1086
|
+
locale: candidate.locale,
|
|
1087
|
+
scopeNamespace,
|
|
1088
|
+
scopePrefix
|
|
1089
|
+
};
|
|
1090
|
+
} };
|
|
1091
|
+
}
|
|
1092
|
+
/** True when a prefix usage may refer to `key`. The key of `t(`pages.${id}`)` is `pages.`, so only children match. */
|
|
1093
|
+
function prefixMatches(usage, key) {
|
|
1094
|
+
return usage.dynamic === "prefix" && usage.key !== void 0 && key.startsWith(usage.key);
|
|
1095
|
+
}
|
|
1096
|
+
/** Ranges covered by `x18n-disable-next-line` and `x18n-disable` ... `x18n-enable` comments. */
|
|
1097
|
+
function suppressedRanges(text) {
|
|
1098
|
+
if (!text.includes("x18n-disable")) return [];
|
|
1099
|
+
const ranges = [];
|
|
1100
|
+
for (const match of text.matchAll(/x18n-disable-next-line/g)) {
|
|
1101
|
+
const lineEnd = text.indexOf("\n", match.index);
|
|
1102
|
+
if (lineEnd === -1) continue;
|
|
1103
|
+
const nextEnd = text.indexOf("\n", lineEnd + 1);
|
|
1104
|
+
ranges.push({
|
|
1105
|
+
start: lineEnd + 1,
|
|
1106
|
+
end: nextEnd === -1 ? text.length : nextEnd
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
for (const match of text.matchAll(/x18n-disable(?![-\w])/g)) {
|
|
1110
|
+
const end = text.indexOf("x18n-enable", match.index);
|
|
1111
|
+
ranges.push({
|
|
1112
|
+
start: match.index,
|
|
1113
|
+
end: end === -1 ? text.length : end
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
return ranges;
|
|
1117
|
+
}
|
|
1118
|
+
/** The plural categories of a locale's language: Japanese has only `other`. */
|
|
1119
|
+
function categoriesOf(info, type) {
|
|
1120
|
+
return type === "ordinal" ? info.ordinalCategories : info.pluralCategories;
|
|
1121
|
+
}
|
|
1122
|
+
const tokens = /(\{key\}|\{plural\}|\{ordinal\}|\*)/;
|
|
1123
|
+
const categories = [
|
|
1124
|
+
"zero",
|
|
1125
|
+
"one",
|
|
1126
|
+
"two",
|
|
1127
|
+
"few",
|
|
1128
|
+
"many",
|
|
1129
|
+
"other"
|
|
1130
|
+
];
|
|
1131
|
+
/**
|
|
1132
|
+
* Compiles derived-key rules (`{key}_plural`, `{key}_*`, `{key}_{plural}`). `*` is any key segment;
|
|
1133
|
+
* `{plural}` and `{ordinal}` are CLDR plural categories, which each locale needs its own set of.
|
|
1134
|
+
*/
|
|
1135
|
+
function derivedKeys(rules) {
|
|
1136
|
+
const compiled = rules.map((rule) => {
|
|
1137
|
+
const parts = rule.split(tokens);
|
|
1138
|
+
const type = parts.includes("{ordinal}") ? "ordinal" : parts.includes("{plural}") ? "cardinal" : void 0;
|
|
1139
|
+
const source = parts.map((part) => {
|
|
1140
|
+
if (part === "{key}") return "(?<key>.+)";
|
|
1141
|
+
if (part === "{plural}" || part === "{ordinal}") return `(?<category>${categories.join("|")})`;
|
|
1142
|
+
if (part === "*") return "[^.:]+";
|
|
1143
|
+
return part.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
1144
|
+
}).join("");
|
|
1145
|
+
return {
|
|
1146
|
+
type,
|
|
1147
|
+
re: new RegExp(`^${source}$`, "d")
|
|
1148
|
+
};
|
|
1149
|
+
});
|
|
1150
|
+
const baseOf = (key) => {
|
|
1151
|
+
for (const { re } of compiled) {
|
|
1152
|
+
const base = re.exec(key)?.groups?.key;
|
|
1153
|
+
if (base) return base;
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
return {
|
|
1157
|
+
basesOf(key) {
|
|
1158
|
+
const bases = [];
|
|
1159
|
+
for (let base = baseOf(key); base !== void 0; base = baseOf(base)) bases.push(base);
|
|
1160
|
+
return bases;
|
|
1161
|
+
},
|
|
1162
|
+
formOf(key) {
|
|
1163
|
+
let best;
|
|
1164
|
+
for (const { type, re } of compiled) {
|
|
1165
|
+
const match = type && re.exec(key);
|
|
1166
|
+
if (!type || !match) continue;
|
|
1167
|
+
const base = match.groups?.key;
|
|
1168
|
+
const category = categories.find((c) => c === match.groups?.category);
|
|
1169
|
+
const at = match.indices?.groups?.category;
|
|
1170
|
+
if (!base || !category || !at || best && best.base.length <= base.length) continue;
|
|
1171
|
+
const [start, end] = at;
|
|
1172
|
+
best = {
|
|
1173
|
+
base,
|
|
1174
|
+
type,
|
|
1175
|
+
category,
|
|
1176
|
+
keyOf: (other) => `${key.slice(0, start)}${other}${key.slice(end)}`
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
return best;
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
//#endregion
|
|
941
1184
|
//#region src/project/analysis.ts
|
|
942
1185
|
function levelOf(setting) {
|
|
943
1186
|
if (setting === void 0) return void 0;
|
|
@@ -950,9 +1193,24 @@ function levelOf(setting) {
|
|
|
950
1193
|
options: setting[1]
|
|
951
1194
|
};
|
|
952
1195
|
}
|
|
953
|
-
|
|
1196
|
+
/**
|
|
1197
|
+
* The keys a locale needs: the reference keys, with plural forms swapped for the categories of the locale's
|
|
1198
|
+
* language. Forms the language lacks (`item_zero` in English) count only where they exist.
|
|
1199
|
+
*/
|
|
1200
|
+
function expectedKeys(state, options, reference, locale) {
|
|
1201
|
+
const info = options.localeInfo(locale);
|
|
1202
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1203
|
+
for (const key of reference) {
|
|
1204
|
+
const form = options.formOf(key);
|
|
1205
|
+
if (!form || state.messages.get(key)?.has(locale)) keys.add(key);
|
|
1206
|
+
form && categoriesOf(info, form.type).forEach((category) => keys.add(form.keyOf(category)));
|
|
1207
|
+
}
|
|
1208
|
+
return [...keys].sort();
|
|
1209
|
+
}
|
|
1210
|
+
function computeCoverage(state, options, sourceKeys) {
|
|
954
1211
|
const { messages, sourceLocale } = state;
|
|
955
1212
|
return state.locales.map((locale) => {
|
|
1213
|
+
const reference = expectedKeys(state, options, sourceKeys, locale);
|
|
956
1214
|
const chain = fallbackChain(state.fallbacks, locale);
|
|
957
1215
|
const inherited = (key) => chain.some((code) => {
|
|
958
1216
|
const value = messages.get(key)?.get(code)?.[0]?.value;
|
|
@@ -1007,7 +1265,7 @@ function* runCheck(check, level, inputs, input) {
|
|
|
1007
1265
|
const severity = config.strict && (item.severity ?? level) === "warning" ? "error" : item.severity ?? level;
|
|
1008
1266
|
let location = item.location;
|
|
1009
1267
|
if (!location && item.key !== void 0) {
|
|
1010
|
-
const byLocale = state.messages.get(item.key);
|
|
1268
|
+
const byLocale = state.messages.get(item.key) ?? state.messages.get(input.ctx.formOf(item.key)?.keyOf("other") ?? item.key);
|
|
1011
1269
|
const found = byLocale?.get(item.locale ?? state.sourceLocale)?.[0] ?? byLocale?.get(state.sourceLocale)?.[0];
|
|
1012
1270
|
if (found) location = {
|
|
1013
1271
|
path: resourcePath(found.resource),
|
|
@@ -1067,7 +1325,7 @@ function createAnalyzer(options) {
|
|
|
1067
1325
|
const { sourceLocale, locales, messages } = state;
|
|
1068
1326
|
const sourceKeys = [...messages.keys()].filter((key) => messages.get(key)?.has(sourceLocale)).sort();
|
|
1069
1327
|
const allKeys = [...messages.keys()].sort();
|
|
1070
|
-
const coverage = computeCoverage(state, sourceKeys.length > 0 ? sourceKeys : allKeys);
|
|
1328
|
+
const coverage = computeCoverage(state, options, sourceKeys.length > 0 ? sourceKeys : allKeys);
|
|
1071
1329
|
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);
|
|
1072
1330
|
const hasScanners = state.files.size > 0;
|
|
1073
1331
|
const referenceSyntaxes = registry.placeholders.filter((syntax) => syntax.reference);
|
|
@@ -1079,8 +1337,7 @@ function createAnalyzer(options) {
|
|
|
1079
1337
|
const prefixes = [...new Set(state.prefixUsages.flatMap((usage) => usage.key === void 0 ? [] : [usage.key]))];
|
|
1080
1338
|
const isUsed = (key) => {
|
|
1081
1339
|
if (state.usagesByKey.has(key) || inUse(key) || referenced.has(key)) return true;
|
|
1082
|
-
|
|
1083
|
-
if (base !== void 0 && (state.usagesByKey.has(base) || inUse(base))) return true;
|
|
1340
|
+
if (options.basesOf(key).some((base) => state.usagesByKey.has(base) || inUse(base))) return true;
|
|
1084
1341
|
return prefixes.some((prefix) => key.startsWith(prefix));
|
|
1085
1342
|
};
|
|
1086
1343
|
const unused = hasScanners ? allKeys.filter((key) => !isUsed(key)) : [];
|
|
@@ -1107,6 +1364,7 @@ function createAnalyzer(options) {
|
|
|
1107
1364
|
},
|
|
1108
1365
|
message: (key, locale) => messages.get(key)?.get(locale)?.[0],
|
|
1109
1366
|
duplicates: (key, locale) => messages.get(key)?.get(locale) ?? [],
|
|
1367
|
+
formOf: options.formOf,
|
|
1110
1368
|
usages: () => state.usages,
|
|
1111
1369
|
coverage: () => coverage,
|
|
1112
1370
|
unused: () => unused,
|
|
@@ -1184,7 +1442,8 @@ function mirrorPath(path, from, to) {
|
|
|
1184
1442
|
function importEdit(text, lines) {
|
|
1185
1443
|
const missing = lines.filter((line) => !text.includes(line.trim()));
|
|
1186
1444
|
if (missing.length === 0) return void 0;
|
|
1187
|
-
const
|
|
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);
|
|
1188
1447
|
let at = 0;
|
|
1189
1448
|
if (script) at = script.index + script[0].length;
|
|
1190
1449
|
else {
|
|
@@ -1278,7 +1537,9 @@ var PlanBuilder = class {
|
|
|
1278
1537
|
const sourceState = source && this.stateById.get(source.resource);
|
|
1279
1538
|
const namespace = namespaced ? this.keyId(key).namespace ?? this.config.namespace.default : sourceState?.resource.namespace;
|
|
1280
1539
|
const exists = (path) => resources.some((r) => r.resource.path === path);
|
|
1281
|
-
const
|
|
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);
|
|
1282
1543
|
if (resolved && !exists(resolved)) {
|
|
1283
1544
|
const format = this.options.registry.formatFor(resolved);
|
|
1284
1545
|
if (format?.serialize) {
|
|
@@ -1294,8 +1555,6 @@ var PlanBuilder = class {
|
|
|
1294
1555
|
return target;
|
|
1295
1556
|
}
|
|
1296
1557
|
}
|
|
1297
|
-
const sibling = (r) => r.resource.locale === locale && r.resource.namespace !== void 0 && namespace !== void 0;
|
|
1298
|
-
const model = (sourceState && fits(sourceState) ? sourceState : void 0) ?? resources.find((s) => s.resource.locale === sourceLocale && fits(s)) ?? resources.find(fits) ?? resources.find(sibling);
|
|
1299
1558
|
const format = model && this.options.registry.formatById(model.resource.format);
|
|
1300
1559
|
let path = model && mirrorPath(model.resource.path, model.resource.locale, locale);
|
|
1301
1560
|
if (model && path && namespace !== void 0 && model.resource.namespace !== void 0 && model.resource.namespace !== namespace) path = mirrorPath(path, model.resource.namespace, namespace);
|
|
@@ -1527,7 +1786,7 @@ var PlanBuilder = class {
|
|
|
1527
1786
|
const all = this.options.allKeys();
|
|
1528
1787
|
const isLeaf = this.options.messages(op.from) !== void 0;
|
|
1529
1788
|
const under = all.filter((key) => isKeyUnder(key, op.from));
|
|
1530
|
-
const derived = all.filter((key) => this.options.
|
|
1789
|
+
const derived = all.filter((key) => this.options.basesOf(key).includes(op.from));
|
|
1531
1790
|
let keys;
|
|
1532
1791
|
if (op.type === "rename-prefix") keys = under;
|
|
1533
1792
|
else if (isLeaf) keys = [op.from, ...derived];
|
|
@@ -1919,95 +2178,6 @@ function renderPrompt(data) {
|
|
|
1919
2178
|
return out.join("\n");
|
|
1920
2179
|
}
|
|
1921
2180
|
//#endregion
|
|
1922
|
-
//#region src/project/usages.ts
|
|
1923
|
-
function innermost(scopes, range) {
|
|
1924
|
-
let best;
|
|
1925
|
-
for (const scope of scopes) if (scope.range.start <= range.start && scope.range.end >= range.end && (!best || scope.range.end - scope.range.start < best.range.end - best.range.start)) best = scope;
|
|
1926
|
-
return best;
|
|
1927
|
-
}
|
|
1928
|
-
function createUsageResolver(config, namespaced, normalizers) {
|
|
1929
|
-
const separator = config.namespace.separator;
|
|
1930
|
-
return { resolve(candidate, scopes, path, scanner) {
|
|
1931
|
-
const scope = innermost(scopes, candidate.range);
|
|
1932
|
-
const ctx = { path };
|
|
1933
|
-
let text = candidate.raw;
|
|
1934
|
-
for (const normalize of normalizers) {
|
|
1935
|
-
const result = normalize(text, ctx);
|
|
1936
|
-
if (result !== void 0) {
|
|
1937
|
-
text = result;
|
|
1938
|
-
break;
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
const scopeNamespace = candidate.namespace ?? scope?.namespace;
|
|
1942
|
-
const scopePrefix = candidate.keyPrefix ?? scope?.keyPrefix;
|
|
1943
|
-
let namespace;
|
|
1944
|
-
let explicit = false;
|
|
1945
|
-
if (namespaced) {
|
|
1946
|
-
const index = text.indexOf(separator);
|
|
1947
|
-
explicit = index > 0;
|
|
1948
|
-
if (explicit) {
|
|
1949
|
-
namespace = text.slice(0, index);
|
|
1950
|
-
text = text.slice(index + separator.length);
|
|
1951
|
-
} else namespace = scopeNamespace ?? config.namespace.default;
|
|
1952
|
-
}
|
|
1953
|
-
if (scopePrefix && !explicit) text = text ? `${scopePrefix}.${text}` : scopePrefix;
|
|
1954
|
-
let key;
|
|
1955
|
-
if (candidate.dynamic === "unknown") key = void 0;
|
|
1956
|
-
else key = joinKey(namespace, text === "" ? [] : text.split("."));
|
|
1957
|
-
return {
|
|
1958
|
-
key,
|
|
1959
|
-
raw: candidate.raw,
|
|
1960
|
-
path,
|
|
1961
|
-
range: candidate.range,
|
|
1962
|
-
scanner,
|
|
1963
|
-
kind: candidate.kind,
|
|
1964
|
-
dynamic: candidate.dynamic,
|
|
1965
|
-
locale: candidate.locale,
|
|
1966
|
-
scopeNamespace,
|
|
1967
|
-
scopePrefix
|
|
1968
|
-
};
|
|
1969
|
-
} };
|
|
1970
|
-
}
|
|
1971
|
-
/** True when a prefix usage may refer to `key`. The key of `t(`pages.${id}`)` is `pages.`, so only children match. */
|
|
1972
|
-
function prefixMatches(usage, key) {
|
|
1973
|
-
return usage.dynamic === "prefix" && usage.key !== void 0 && key.startsWith(usage.key);
|
|
1974
|
-
}
|
|
1975
|
-
/** Ranges covered by `x18n-disable-next-line` and `x18n-disable` ... `x18n-enable` comments. */
|
|
1976
|
-
function suppressedRanges(text) {
|
|
1977
|
-
if (!text.includes("x18n-disable")) return [];
|
|
1978
|
-
const ranges = [];
|
|
1979
|
-
for (const match of text.matchAll(/x18n-disable-next-line/g)) {
|
|
1980
|
-
const lineEnd = text.indexOf("\n", match.index);
|
|
1981
|
-
if (lineEnd === -1) continue;
|
|
1982
|
-
const nextEnd = text.indexOf("\n", lineEnd + 1);
|
|
1983
|
-
ranges.push({
|
|
1984
|
-
start: lineEnd + 1,
|
|
1985
|
-
end: nextEnd === -1 ? text.length : nextEnd
|
|
1986
|
-
});
|
|
1987
|
-
}
|
|
1988
|
-
for (const match of text.matchAll(/x18n-disable(?![-\w])/g)) {
|
|
1989
|
-
const end = text.indexOf("x18n-enable", match.index);
|
|
1990
|
-
ranges.push({
|
|
1991
|
-
start: match.index,
|
|
1992
|
-
end: end === -1 ? text.length : end
|
|
1993
|
-
});
|
|
1994
|
-
}
|
|
1995
|
-
return ranges;
|
|
1996
|
-
}
|
|
1997
|
-
/** Compiles derived-key rules (`{key}_plural`, `{key}_*`) into a function from a key to its base key. */
|
|
1998
|
-
function derivedBase(rules) {
|
|
1999
|
-
const regexes = rules.map((rule) => {
|
|
2000
|
-
const source = rule.split("{key}").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^.:]+")).join("(.+)");
|
|
2001
|
-
return new RegExp(`^${source}$`);
|
|
2002
|
-
});
|
|
2003
|
-
return (key) => {
|
|
2004
|
-
for (const re of regexes) {
|
|
2005
|
-
const match = re.exec(key);
|
|
2006
|
-
if (match?.[1]) return match[1];
|
|
2007
|
-
}
|
|
2008
|
-
};
|
|
2009
|
-
}
|
|
2010
|
-
//#endregion
|
|
2011
2181
|
//#region src/project/project.ts
|
|
2012
2182
|
const scanConcurrency = 64;
|
|
2013
2183
|
function createProject(options) {
|
|
@@ -2033,7 +2203,7 @@ function createProject(options) {
|
|
|
2033
2203
|
const normalizers = registry.plugins.flatMap((plugin) => plugin.normalizeKey ? [plugin.normalizeKey] : []);
|
|
2034
2204
|
const transforms = registry.plugins.flatMap((plugin) => plugin.transformResource ? [plugin.transformResource] : []);
|
|
2035
2205
|
const localeInfoHooks = registry.plugins.flatMap((plugin) => plugin.localeInfo ? [plugin.localeInfo] : []);
|
|
2036
|
-
const
|
|
2206
|
+
const { basesOf, formOf } = derivedKeys(config.usage.derivedKeys);
|
|
2037
2207
|
const scanSignature = hashText(JSON.stringify({
|
|
2038
2208
|
usage: config.usage,
|
|
2039
2209
|
namespace: config.namespace,
|
|
@@ -2061,6 +2231,11 @@ function createProject(options) {
|
|
|
2061
2231
|
}
|
|
2062
2232
|
return info;
|
|
2063
2233
|
}
|
|
2234
|
+
/** Whether `locale` needs `key`: not a plural form of a category its language lacks. */
|
|
2235
|
+
function needs(key, locale) {
|
|
2236
|
+
const form = formOf(key);
|
|
2237
|
+
return !form || categoriesOf(localeInfo(locale), form.type).includes(form.category);
|
|
2238
|
+
}
|
|
2064
2239
|
async function discover() {
|
|
2065
2240
|
const ctx = {
|
|
2066
2241
|
host,
|
|
@@ -2188,6 +2363,22 @@ function createProject(options) {
|
|
|
2188
2363
|
if (state.error) return void 0;
|
|
2189
2364
|
return new Map([...walkLeaves(state.tree)].map((leaf) => [keyFor(state.resource, leaf.path), leaf.value]));
|
|
2190
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
|
+
}
|
|
2191
2382
|
async function loadResources(candidates, reread) {
|
|
2192
2383
|
const next = /* @__PURE__ */ new Map();
|
|
2193
2384
|
const paths = [...new Set(candidates.map((c) => c.path))];
|
|
@@ -2199,6 +2390,7 @@ function createProject(options) {
|
|
|
2199
2390
|
}
|
|
2200
2391
|
}));
|
|
2201
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)));
|
|
2202
2394
|
for (const candidate of candidates) {
|
|
2203
2395
|
const text = fileTexts.get(candidate.path);
|
|
2204
2396
|
if (text === void 0) continue;
|
|
@@ -2219,6 +2411,11 @@ function createProject(options) {
|
|
|
2219
2411
|
resolver = createUsageResolver(config, namespaced, normalizers);
|
|
2220
2412
|
resolved = /* @__PURE__ */ new WeakMap();
|
|
2221
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
|
+
}));
|
|
2222
2419
|
}
|
|
2223
2420
|
async function scanOne(file) {
|
|
2224
2421
|
const usages = [];
|
|
@@ -2226,7 +2423,7 @@ function createProject(options) {
|
|
|
2226
2423
|
const keySlots = [];
|
|
2227
2424
|
const hardStrings = [];
|
|
2228
2425
|
const calls = config.usage.calls.filter((spec) => ![spec.callee].flat().some((name) => config.usage.ignoreCallees.includes(name)));
|
|
2229
|
-
for (const scanner of registry.
|
|
2426
|
+
for (const scanner of await registry.loadScannersFor(file.path)) {
|
|
2230
2427
|
const result = await scanner.scan(file, {
|
|
2231
2428
|
config,
|
|
2232
2429
|
calls
|
|
@@ -2379,13 +2576,10 @@ function createProject(options) {
|
|
|
2379
2576
|
}
|
|
2380
2577
|
for (const { namespace, path } of keyPaths.values()) for (let i = 1; i < path.length; i++) parents.add(joinKey(namespace, path.slice(0, i)));
|
|
2381
2578
|
const derived = /* @__PURE__ */ new Map();
|
|
2382
|
-
if (config.usage.derivedKeys.length > 0) for (const key of messages.keys()) {
|
|
2383
|
-
const
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
derived.set(base, list);
|
|
2387
|
-
list.push(key);
|
|
2388
|
-
}
|
|
2579
|
+
if (config.usage.derivedKeys.length > 0) for (const key of messages.keys()) for (const base of basesOf(key)) {
|
|
2580
|
+
const list = derived.get(base) ?? [];
|
|
2581
|
+
derived.set(base, list);
|
|
2582
|
+
list.push(key);
|
|
2389
2583
|
}
|
|
2390
2584
|
return {
|
|
2391
2585
|
messages,
|
|
@@ -2396,12 +2590,13 @@ function createProject(options) {
|
|
|
2396
2590
|
};
|
|
2397
2591
|
}
|
|
2398
2592
|
function buildIndex() {
|
|
2399
|
-
resourceIndex ??= buildResourceIndex();
|
|
2593
|
+
const { messages, parents, derived } = resourceIndex ??= buildResourceIndex();
|
|
2594
|
+
const has = (key) => messages.has(key) || parents.has(key) || derived.has(key);
|
|
2400
2595
|
const usages = [];
|
|
2401
2596
|
for (const [path, state] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
2402
2597
|
let list = resolved.get(state);
|
|
2403
2598
|
if (!list) {
|
|
2404
|
-
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));
|
|
2405
2600
|
resolved.set(state, list);
|
|
2406
2601
|
}
|
|
2407
2602
|
usages.push(...list);
|
|
@@ -2457,8 +2652,7 @@ function createProject(options) {
|
|
|
2457
2652
|
const inPath = (usage) => query.path === void 0 || usage.path === query.path;
|
|
2458
2653
|
if (query.key !== void 0) {
|
|
2459
2654
|
const key = query.key;
|
|
2460
|
-
|
|
2461
|
-
return [...[...usagesByKey.get(key) ?? [], ...base ? usagesByKey.get(base) ?? [] : []].filter(inPath).map((usage) => ({
|
|
2655
|
+
return [...[key, ...basesOf(key)].flatMap((k) => usagesByKey.get(k) ?? []).filter(inPath).map((usage) => ({
|
|
2462
2656
|
usage,
|
|
2463
2657
|
possible: false
|
|
2464
2658
|
})), ...prefixUsages.filter((usage) => inPath(usage) && prefixMatches(usage, key)).map((usage) => ({
|
|
@@ -2508,7 +2702,8 @@ function createProject(options) {
|
|
|
2508
2702
|
files,
|
|
2509
2703
|
history: history.get
|
|
2510
2704
|
}),
|
|
2511
|
-
|
|
2705
|
+
basesOf,
|
|
2706
|
+
formOf,
|
|
2512
2707
|
localeInfo,
|
|
2513
2708
|
text: (path) => fileTexts.get(path) ?? files.get(path)?.text
|
|
2514
2709
|
});
|
|
@@ -2522,7 +2717,7 @@ function createProject(options) {
|
|
|
2522
2717
|
const locale = query.missing;
|
|
2523
2718
|
list = list.filter((key) => {
|
|
2524
2719
|
const value = message(key, locale)?.value;
|
|
2525
|
-
return message(key, sourceLocale) !== void 0 && (value === void 0 || value === "");
|
|
2720
|
+
return message(key, sourceLocale) !== void 0 && (value === void 0 || value === "") && needs(key, locale);
|
|
2526
2721
|
});
|
|
2527
2722
|
}
|
|
2528
2723
|
if (query.unused) {
|
|
@@ -2542,7 +2737,7 @@ function createProject(options) {
|
|
|
2542
2737
|
namespace: id.namespace,
|
|
2543
2738
|
path: id.path,
|
|
2544
2739
|
messages: list,
|
|
2545
|
-
missing: locales.filter((locale) => !byLocale.has(locale)),
|
|
2740
|
+
missing: locales.filter((locale) => !byLocale.has(locale) && needs(key, locale)),
|
|
2546
2741
|
usages: usageMatches({ key }),
|
|
2547
2742
|
diagnostics: analyzer.analyze().diagnostics.filter((d) => d.key === key)
|
|
2548
2743
|
};
|
|
@@ -2634,8 +2829,9 @@ function createProject(options) {
|
|
|
2634
2829
|
const before = idx();
|
|
2635
2830
|
const paths = new Set(events.map((event) => event.path));
|
|
2636
2831
|
const resourcePaths = new Set([...resourceStates.values()].map((state) => state.resource.path));
|
|
2637
|
-
const maybeResource = (path) => !resourcePaths.has(path) && registry.
|
|
2638
|
-
const
|
|
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));
|
|
2639
2835
|
if (structural || [...paths].some((path) => resourcePaths.has(path))) await loadResources(structural ? await discover() : [...resourceStates.values()].map((state) => ({
|
|
2640
2836
|
...state.resource,
|
|
2641
2837
|
format: state.resource.format === "unknown" ? void 0 : state.resource.format
|
|
@@ -2674,22 +2870,9 @@ function createProject(options) {
|
|
|
2674
2870
|
parents: () => idx().parents,
|
|
2675
2871
|
usageMatches,
|
|
2676
2872
|
text: (path) => fileTexts.get(path) ?? files.get(path)?.text,
|
|
2677
|
-
|
|
2873
|
+
basesOf,
|
|
2678
2874
|
toStored: (target, path) => storedPathOf(target)(path),
|
|
2679
|
-
resolveNew
|
|
2680
|
-
const ctx = {
|
|
2681
|
-
host,
|
|
2682
|
-
config,
|
|
2683
|
-
extensions: [...new Set(registry.formats.flatMap((format) => format.extensions))]
|
|
2684
|
-
};
|
|
2685
|
-
for (const finder of registry.finders) {
|
|
2686
|
-
const path = finder.resolveNew?.({
|
|
2687
|
-
locale,
|
|
2688
|
-
namespace
|
|
2689
|
-
}, ctx);
|
|
2690
|
-
if (path) return path;
|
|
2691
|
-
}
|
|
2692
|
-
},
|
|
2875
|
+
resolveNew: newResourcePath,
|
|
2693
2876
|
update
|
|
2694
2877
|
});
|
|
2695
2878
|
const ready = (async () => {
|
|
@@ -2748,7 +2931,7 @@ function createProject(options) {
|
|
|
2748
2931
|
lookup(key, locale) {
|
|
2749
2932
|
const exact = message(key, locale);
|
|
2750
2933
|
if (exact) return exact;
|
|
2751
|
-
const derived =
|
|
2934
|
+
const derived = (idx().derived.get(key) ?? []).filter((k) => message(k, locale) !== void 0).sort((a, b) => a.length - b.length || a.localeCompare(b));
|
|
2752
2935
|
const other = derived.find((k) => /[._-]other$/.test(k)) ?? derived[0];
|
|
2753
2936
|
return other === void 0 ? void 0 : message(other, locale);
|
|
2754
2937
|
},
|
|
@@ -2820,6 +3003,8 @@ function defaultDraft() {
|
|
|
2820
3003
|
namespace: {
|
|
2821
3004
|
separator: ".",
|
|
2822
3005
|
default: void 0,
|
|
3006
|
+
fallback: [],
|
|
3007
|
+
byFile: {},
|
|
2823
3008
|
fromPath: "dot"
|
|
2824
3009
|
},
|
|
2825
3010
|
usage: {
|
|
@@ -2903,6 +3088,11 @@ function applyLayer(draft, layer, origin, origins) {
|
|
|
2903
3088
|
draft.namespace = {
|
|
2904
3089
|
separator: layer.namespace.separator ?? draft.namespace.separator,
|
|
2905
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
|
+
},
|
|
2906
3096
|
fromPath: layer.namespace.fromPath ?? draft.namespace.fromPath
|
|
2907
3097
|
};
|
|
2908
3098
|
mark("namespace");
|
|
@@ -3007,8 +3197,8 @@ function createLoader(options) {
|
|
|
3007
3197
|
if (option === false || option === void 0) return void 0;
|
|
3008
3198
|
if (typeof option === "object" && !Array.isArray(option)) return option;
|
|
3009
3199
|
const [specifier, pluginOptions] = typeof option === "string" ? [option, void 0] : [option[0], option[1]];
|
|
3010
|
-
const
|
|
3011
|
-
if (
|
|
3200
|
+
const entry = options.builtins.presets[specifier];
|
|
3201
|
+
if (entry) return (await entry.load()).default(pluginOptions);
|
|
3012
3202
|
if (!options.loadPlugin) throw errors.X18N1003({ preset: specifier });
|
|
3013
3203
|
return options.loadPlugin(specifier, pluginOptions);
|
|
3014
3204
|
};
|
|
@@ -3044,7 +3234,17 @@ async function expandPresets(presets, userPlugins, listed, detectCtx) {
|
|
|
3044
3234
|
name: preset.name,
|
|
3045
3235
|
reason: "after-exclusive"
|
|
3046
3236
|
});
|
|
3047
|
-
else
|
|
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);
|
|
3048
3248
|
for (const plugin of userPlugins) await flatten(plugin, true);
|
|
3049
3249
|
return {
|
|
3050
3250
|
plugins,
|
|
@@ -3067,7 +3267,10 @@ async function resolveConfig(options) {
|
|
|
3067
3267
|
const userPlugins = await loadAll(load, [...options.parent?.plugins ?? [], ...user.plugins ?? []]);
|
|
3068
3268
|
const presetOption = user.presets ?? options.parent?.presets ?? "auto";
|
|
3069
3269
|
const auto = presetOption === "auto";
|
|
3070
|
-
const presets = auto ? Object.
|
|
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 : {
|
|
3071
3274
|
name: item.name,
|
|
3072
3275
|
plugins: [item]
|
|
3073
3276
|
});
|
|
@@ -3108,4 +3311,4 @@ async function resolveConfig(options) {
|
|
|
3108
3311
|
};
|
|
3109
3312
|
}
|
|
3110
3313
|
//#endregion
|
|
3111
|
-
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
package/schema/config.json
CHANGED
|
@@ -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
|
-
"
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
"
|
|
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
|
-
|
|
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",
|
|
@@ -404,6 +426,12 @@
|
|
|
404
426
|
}
|
|
405
427
|
}
|
|
406
428
|
},
|
|
429
|
+
"attributes": {
|
|
430
|
+
"type": "array",
|
|
431
|
+
"items": {
|
|
432
|
+
"type": "string"
|
|
433
|
+
}
|
|
434
|
+
},
|
|
407
435
|
"ignoreAttributes": {
|
|
408
436
|
"type": "array",
|
|
409
437
|
"items": {
|
package/schema/plugin.json
CHANGED
|
@@ -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
|
-
"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"
|
|
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
|
-
|
|
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",
|