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