@contractspec/lib.contracts 0.0.0-canary-20260119225944 → 0.0.0-canary-20260202053150
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/app-config/{contracts.d.ts → app-config.contracts.d.ts} +52 -52
- package/dist/app-config/{contracts.js → app-config.contracts.js} +1 -1
- package/dist/app-config/lifecycle-contracts.d.ts +55 -55
- package/dist/app-config/runtime.d.ts +4 -4
- package/dist/app-config/spec.d.ts +2 -2
- package/dist/capabilities/capabilities.d.ts +64 -5
- package/dist/capabilities/capabilities.js +125 -0
- package/dist/capabilities/context.d.ts +88 -0
- package/dist/capabilities/context.js +87 -0
- package/dist/capabilities/docs/capabilities.docblock.js +191 -2
- package/dist/capabilities/guards.d.ts +110 -0
- package/dist/capabilities/guards.js +146 -0
- package/dist/capabilities/index.d.ts +4 -1
- package/dist/capabilities/index.js +4 -1
- package/dist/capabilities/validation.d.ts +76 -0
- package/dist/capabilities/validation.js +141 -0
- package/dist/client/react/feature-render.d.ts +2 -2
- package/dist/data-views/data-views.d.ts +2 -1
- package/dist/data-views/index.d.ts +2 -1
- package/dist/data-views/registry.d.ts +1 -1
- package/dist/data-views/report/contractVerificationTable.d.ts +10 -0
- package/dist/data-views/report/contractVerificationTable.js +95 -0
- package/dist/data-views/runtime.d.ts +2 -2
- package/dist/data-views/spec.d.ts +2 -8
- package/dist/data-views/types.d.ts +1 -1
- package/dist/docs/capabilities/documentationSystem.capability.d.ts +7 -0
- package/dist/docs/capabilities/documentationSystem.capability.js +71 -0
- package/dist/docs/capabilities/index.d.ts +2 -0
- package/dist/docs/capabilities/index.js +3 -0
- package/dist/docs/commands/docsGenerate.command.d.ts +95 -0
- package/dist/docs/commands/docsGenerate.command.js +142 -0
- package/dist/docs/commands/docsPublish.command.d.ts +64 -0
- package/dist/docs/commands/docsPublish.command.js +107 -0
- package/dist/docs/commands/index.d.ts +3 -0
- package/dist/docs/commands/index.js +4 -0
- package/dist/docs/constants.d.ts +7 -0
- package/dist/docs/constants.js +10 -0
- package/dist/docs/contracts.d.ts +442 -0
- package/dist/docs/contracts.js +58 -0
- package/dist/docs/ensure-docblocks.d.ts +1 -0
- package/dist/docs/ensure-docblocks.js +1 -0
- package/dist/docs/events/docsGenerated.event.d.ts +62 -0
- package/dist/docs/events/docsGenerated.event.js +53 -0
- package/dist/docs/events/docsPublished.event.d.ts +70 -0
- package/dist/docs/events/docsPublished.event.js +57 -0
- package/dist/docs/events/index.d.ts +3 -0
- package/dist/docs/events/index.js +4 -0
- package/dist/docs/forms/docsSearch.form.d.ts +22 -0
- package/dist/docs/forms/docsSearch.form.js +113 -0
- package/dist/docs/forms/index.d.ts +2 -0
- package/dist/docs/forms/index.js +3 -0
- package/dist/docs/index.d.ts +23 -2
- package/dist/docs/index.js +25 -1
- package/dist/docs/presentations/docsLayout.presentation.d.ts +7 -0
- package/dist/docs/presentations/docsLayout.presentation.js +35 -0
- package/dist/docs/presentations/docsReferencePage.presentation.d.ts +7 -0
- package/dist/docs/presentations/docsReferencePage.presentation.js +35 -0
- package/dist/docs/presentations/index.d.ts +3 -0
- package/dist/docs/presentations/index.js +4 -0
- package/dist/docs/queries/contractReference.query.d.ts +217 -0
- package/dist/docs/queries/contractReference.query.js +125 -0
- package/dist/docs/queries/docsIndex.query.d.ts +272 -0
- package/dist/docs/queries/docsIndex.query.js +133 -0
- package/dist/docs/queries/index.d.ts +3 -0
- package/dist/docs/queries/index.js +4 -0
- package/dist/docs/tech/cli.docblock.js +10 -0
- package/dist/docs/tech/docs-system.docblock.d.ts +1 -0
- package/dist/docs/tech/docs-system.docblock.js +128 -0
- package/dist/docs/tech/report-verification-table.docblock.d.ts +1 -0
- package/dist/docs/tech/report-verification-table.docblock.js +50 -0
- package/dist/docs/views/contractReference.dataView.d.ts +7 -0
- package/dist/docs/views/contractReference.dataView.js +80 -0
- package/dist/docs/views/docsIndex.dataView.d.ts +7 -0
- package/dist/docs/views/docsIndex.dataView.js +136 -0
- package/dist/docs/views/exampleCatalog.dataView.d.ts +7 -0
- package/dist/docs/views/exampleCatalog.dataView.js +91 -0
- package/dist/docs/views/index.d.ts +4 -0
- package/dist/docs/views/index.js +5 -0
- package/dist/events.d.ts +80 -14
- package/dist/events.js +33 -3
- package/dist/examples/schema.d.ts +12 -12
- package/dist/examples/types.d.ts +1 -1
- package/dist/experiments/spec.d.ts +9 -6
- package/dist/features/index.d.ts +2 -2
- package/dist/features/install.d.ts +4 -4
- package/dist/features/types.d.ts +28 -32
- package/dist/forms/forms.d.ts +1 -1
- package/dist/index.d.ts +60 -31
- package/dist/index.js +32 -4
- package/dist/install.d.ts +1 -1
- package/dist/integrations/openbanking/contracts/accounts.d.ts +67 -67
- package/dist/integrations/openbanking/contracts/balances.d.ts +35 -35
- package/dist/integrations/openbanking/contracts/transactions.d.ts +49 -49
- package/dist/integrations/openbanking/models.d.ts +55 -55
- package/dist/integrations/operations.d.ts +103 -103
- package/dist/integrations/spec.d.ts +2 -2
- package/dist/jsonschema.d.ts +1 -1
- package/dist/knowledge/operations.d.ts +67 -67
- package/dist/knowledge/spec.d.ts +1 -1
- package/dist/llm/exporters.d.ts +4 -4
- package/dist/llm/types.d.ts +1 -1
- package/dist/markdown.d.ts +2 -2
- package/dist/operations/index.d.ts +4 -1
- package/dist/operations/index.js +4 -1
- package/dist/operations/operation.d.ts +8 -2
- package/dist/operations/registry.d.ts +1 -1
- package/dist/operations/report/getContractVerificationStatus.d.ts +75 -0
- package/dist/operations/report/getContractVerificationStatus.js +96 -0
- package/dist/operations/report/index.d.ts +13 -0
- package/dist/operations/report/index.js +45 -0
- package/dist/ownership.d.ts +133 -8
- package/dist/ownership.js +25 -0
- package/dist/policy/context.d.ts +237 -0
- package/dist/policy/context.js +227 -0
- package/dist/policy/guards.d.ts +145 -0
- package/dist/policy/guards.js +254 -0
- package/dist/policy/index.d.ts +12 -1
- package/dist/policy/index.js +11 -1
- package/dist/policy/spec.d.ts +7 -4
- package/dist/policy/validation.d.ts +67 -0
- package/dist/policy/validation.js +307 -0
- package/dist/presentations/presentations.d.ts +6 -0
- package/dist/presentations/registry.d.ts +1 -1
- package/dist/registry.d.ts +1 -1
- package/dist/serialization/index.d.ts +3 -0
- package/dist/serialization/index.js +3 -0
- package/dist/serialization/serializers.d.ts +40 -0
- package/dist/serialization/serializers.js +148 -0
- package/dist/serialization/types.d.ts +103 -0
- package/dist/serialization/types.js +0 -0
- package/dist/server/rest-elysia.d.ts +1 -1
- package/dist/server/rest-express.d.ts +1 -1
- package/dist/server/rest-generic.d.ts +1 -1
- package/dist/server/rest-next-app.d.ts +1 -1
- package/dist/server/rest-next-mcp.d.ts +1 -1
- package/dist/server/rest-next-pages.d.ts +1 -1
- package/dist/telemetry/spec.d.ts +1 -1
- package/dist/telemetry/tracker.d.ts +3 -2
- package/dist/tests/runner.d.ts +1 -1
- package/dist/tests/spec.d.ts +17 -12
- package/dist/themes.d.ts +8 -5
- package/dist/translations/index.d.ts +6 -0
- package/dist/translations/index.js +5 -0
- package/dist/translations/registry.d.ts +144 -0
- package/dist/translations/registry.js +223 -0
- package/dist/translations/spec.d.ts +126 -0
- package/dist/translations/spec.js +31 -0
- package/dist/translations/validation.d.ts +85 -0
- package/dist/translations/validation.js +328 -0
- package/dist/types.d.ts +142 -16
- package/dist/versioning/index.d.ts +2 -1
- package/dist/versioning/index.js +2 -1
- package/dist/versioning/refs.d.ts +179 -0
- package/dist/versioning/refs.js +161 -0
- package/dist/workflow/context.d.ts +191 -0
- package/dist/workflow/context.js +227 -0
- package/dist/workflow/index.d.ts +6 -3
- package/dist/workflow/index.js +4 -2
- package/dist/workflow/spec.d.ts +4 -11
- package/dist/workflow/validation.d.ts +64 -2
- package/dist/workflow/validation.js +194 -1
- package/dist/workspace-config/contractsrc-schema.d.ts +3 -3
- package/dist/workspace-config/contractsrc-schema.js +1 -0
- package/dist/workspace-config/contractsrc-types.d.ts +7 -8
- package/dist/workspace-config/index.d.ts +2 -2
- package/package.json +52 -9
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Owner, Stability, Tag } from "../ownership.js";
|
|
2
|
+
|
|
3
|
+
//#region src/translations/spec.d.ts
|
|
4
|
+
|
|
5
|
+
/** ISO 639-1 locale code (e.g., 'en', 'fr', 'de'). */
|
|
6
|
+
type Locale = string;
|
|
7
|
+
/** Message key identifier. */
|
|
8
|
+
type MessageKey = string;
|
|
9
|
+
type PlaceholderType = 'string' | 'number' | 'date' | 'time' | 'datetime' | 'currency' | 'percent' | 'plural' | 'select' | 'selectordinal';
|
|
10
|
+
interface PlaceholderDef {
|
|
11
|
+
/** Placeholder name (without braces). */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Type of the placeholder value. */
|
|
14
|
+
type: PlaceholderType;
|
|
15
|
+
/** Format specifier (e.g., 'short', 'long', 'currency'). */
|
|
16
|
+
format?: string;
|
|
17
|
+
/** Description for translators. */
|
|
18
|
+
description?: string;
|
|
19
|
+
/** Example value for context. */
|
|
20
|
+
example?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* CLDR plural categories.
|
|
24
|
+
* @see https://cldr.unicode.org/index/cldr-spec/plural-rules
|
|
25
|
+
*/
|
|
26
|
+
type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
|
|
27
|
+
interface PluralRule {
|
|
28
|
+
category: PluralCategory;
|
|
29
|
+
value: string;
|
|
30
|
+
}
|
|
31
|
+
interface PluralRuleSet {
|
|
32
|
+
/** Name of the plural variable. */
|
|
33
|
+
variable: string;
|
|
34
|
+
/** Rules for each plural category. */
|
|
35
|
+
rules: PluralRule[];
|
|
36
|
+
}
|
|
37
|
+
type VariantType = 'gender' | 'formality' | 'context';
|
|
38
|
+
interface MessageVariant {
|
|
39
|
+
/** Type of variant (gender, formality, context). */
|
|
40
|
+
type: VariantType;
|
|
41
|
+
/** Variant key (e.g., 'male', 'female', 'formal', 'informal'). */
|
|
42
|
+
key: string;
|
|
43
|
+
/** Variant value (the translated message for this variant). */
|
|
44
|
+
value: string;
|
|
45
|
+
}
|
|
46
|
+
interface TranslationMessage {
|
|
47
|
+
/** The translated value (may use ICU message format). */
|
|
48
|
+
value: string;
|
|
49
|
+
/** Description for translators explaining context and usage. */
|
|
50
|
+
description?: string;
|
|
51
|
+
/** Usage context to help translators understand where this appears. */
|
|
52
|
+
context?: string;
|
|
53
|
+
/** Placeholder definitions for dynamic values. */
|
|
54
|
+
placeholders?: PlaceholderDef[];
|
|
55
|
+
/** Variants for gender, formality, or other contextual differences. */
|
|
56
|
+
variants?: MessageVariant[];
|
|
57
|
+
/** Maximum character length (hint for UI constraints). */
|
|
58
|
+
maxLength?: number;
|
|
59
|
+
/** Tags for categorization and filtering. */
|
|
60
|
+
tags?: string[];
|
|
61
|
+
}
|
|
62
|
+
type TranslationMessages = Record<MessageKey, TranslationMessage>;
|
|
63
|
+
interface TranslationMeta {
|
|
64
|
+
/** Unique key for this translation spec. */
|
|
65
|
+
key: string;
|
|
66
|
+
/** Semantic version (e.g., "1.0.0"). */
|
|
67
|
+
version: string;
|
|
68
|
+
/** Business domain this translation belongs to. */
|
|
69
|
+
domain: string;
|
|
70
|
+
/** Description of the translation bundle. */
|
|
71
|
+
description?: string;
|
|
72
|
+
/** Owners responsible for this translation spec. */
|
|
73
|
+
owners: Owner[];
|
|
74
|
+
/** Stability marker. */
|
|
75
|
+
stability?: Stability;
|
|
76
|
+
/** Tags for categorization. */
|
|
77
|
+
tags?: Tag[];
|
|
78
|
+
}
|
|
79
|
+
interface TranslationSpec {
|
|
80
|
+
/** Metadata about this translation spec. */
|
|
81
|
+
meta: TranslationMeta;
|
|
82
|
+
/** Primary locale for this translation spec. */
|
|
83
|
+
locale: Locale;
|
|
84
|
+
/** Fallback locale when a message is missing. */
|
|
85
|
+
fallback?: Locale;
|
|
86
|
+
/** Translation messages keyed by message key. */
|
|
87
|
+
messages: TranslationMessages;
|
|
88
|
+
/** Plural rules for the locale. */
|
|
89
|
+
pluralRules?: PluralRuleSet[];
|
|
90
|
+
/** Tags for categorization. */
|
|
91
|
+
tags?: string[];
|
|
92
|
+
}
|
|
93
|
+
interface TranslationRef {
|
|
94
|
+
key: string;
|
|
95
|
+
version: string;
|
|
96
|
+
locale?: Locale;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Helper to define a Translation spec.
|
|
100
|
+
*
|
|
101
|
+
* @param spec - Translation specification
|
|
102
|
+
* @returns The same spec with type inference
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* const messages = defineTranslation({
|
|
107
|
+
* meta: {
|
|
108
|
+
* key: 'auth.messages',
|
|
109
|
+
* version: '1.0.0',
|
|
110
|
+
* domain: 'auth',
|
|
111
|
+
* owners: [{ team: 'platform' }],
|
|
112
|
+
* },
|
|
113
|
+
* locale: 'en',
|
|
114
|
+
* messages: {
|
|
115
|
+
* 'login.success': {
|
|
116
|
+
* value: 'Welcome back, {name}!',
|
|
117
|
+
* description: 'Shown after successful login',
|
|
118
|
+
* placeholders: [{ name: 'name', type: 'string' }],
|
|
119
|
+
* },
|
|
120
|
+
* },
|
|
121
|
+
* });
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
declare const defineTranslation: (spec: TranslationSpec) => TranslationSpec;
|
|
125
|
+
//#endregion
|
|
126
|
+
export { Locale, MessageKey, MessageVariant, PlaceholderDef, PlaceholderType, PluralCategory, PluralRule, PluralRuleSet, TranslationMessage, TranslationMessages, TranslationMeta, TranslationRef, TranslationSpec, VariantType, defineTranslation };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/translations/spec.ts
|
|
2
|
+
/**
|
|
3
|
+
* Helper to define a Translation spec.
|
|
4
|
+
*
|
|
5
|
+
* @param spec - Translation specification
|
|
6
|
+
* @returns The same spec with type inference
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const messages = defineTranslation({
|
|
11
|
+
* meta: {
|
|
12
|
+
* key: 'auth.messages',
|
|
13
|
+
* version: '1.0.0',
|
|
14
|
+
* domain: 'auth',
|
|
15
|
+
* owners: [{ team: 'platform' }],
|
|
16
|
+
* },
|
|
17
|
+
* locale: 'en',
|
|
18
|
+
* messages: {
|
|
19
|
+
* 'login.success': {
|
|
20
|
+
* value: 'Welcome back, {name}!',
|
|
21
|
+
* description: 'Shown after successful login',
|
|
22
|
+
* placeholders: [{ name: 'name', type: 'string' }],
|
|
23
|
+
* },
|
|
24
|
+
* },
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
const defineTranslation = (spec) => spec;
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { defineTranslation };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Locale, MessageKey, TranslationMessage, TranslationSpec } from "./spec.js";
|
|
2
|
+
import { TranslationRegistry } from "./registry.js";
|
|
3
|
+
|
|
4
|
+
//#region src/translations/validation.d.ts
|
|
5
|
+
|
|
6
|
+
type TranslationValidationLevel = 'error' | 'warning' | 'info';
|
|
7
|
+
interface TranslationValidationIssue {
|
|
8
|
+
level: TranslationValidationLevel;
|
|
9
|
+
message: string;
|
|
10
|
+
path?: string;
|
|
11
|
+
context?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
interface TranslationValidationResult {
|
|
14
|
+
valid: boolean;
|
|
15
|
+
issues: TranslationValidationIssue[];
|
|
16
|
+
}
|
|
17
|
+
interface ICUValidationResult {
|
|
18
|
+
valid: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
placeholders: string[];
|
|
21
|
+
plurals: string[];
|
|
22
|
+
selects: string[];
|
|
23
|
+
}
|
|
24
|
+
interface MissingTranslation {
|
|
25
|
+
messageKey: MessageKey;
|
|
26
|
+
sourceLocale: Locale;
|
|
27
|
+
targetLocale: Locale;
|
|
28
|
+
sourceMessage: TranslationMessage;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Validate a translation spec for internal consistency.
|
|
32
|
+
*
|
|
33
|
+
* @param spec - Translation spec to validate
|
|
34
|
+
* @returns Validation result with any issues found
|
|
35
|
+
*/
|
|
36
|
+
declare function validateTranslationSpec(spec: TranslationSpec): TranslationValidationResult;
|
|
37
|
+
/**
|
|
38
|
+
* Validate ICU message format syntax.
|
|
39
|
+
*
|
|
40
|
+
* This is a simplified validator that checks for balanced braces
|
|
41
|
+
* and extracts placeholders, plurals, and selects.
|
|
42
|
+
*
|
|
43
|
+
* @param message - Message string to validate
|
|
44
|
+
* @returns Validation result with extracted components
|
|
45
|
+
*/
|
|
46
|
+
declare function validateICUFormat(message: string): ICUValidationResult;
|
|
47
|
+
/**
|
|
48
|
+
* Find messages that exist in source but not in target.
|
|
49
|
+
*
|
|
50
|
+
* @param sourceSpec - Source translation spec (usually base locale like 'en')
|
|
51
|
+
* @param targetSpec - Target translation spec to check
|
|
52
|
+
* @returns Array of missing translations
|
|
53
|
+
*/
|
|
54
|
+
declare function findMissingTranslations(sourceSpec: TranslationSpec, targetSpec: TranslationSpec): MissingTranslation[];
|
|
55
|
+
/**
|
|
56
|
+
* Find all missing translations across a registry.
|
|
57
|
+
*
|
|
58
|
+
* Compares each locale against a base locale to find missing keys.
|
|
59
|
+
*
|
|
60
|
+
* @param registry - Translation registry
|
|
61
|
+
* @param specKey - Translation spec key to check
|
|
62
|
+
* @param baseLocale - Base locale to compare against
|
|
63
|
+
* @returns Map of locale to missing translations
|
|
64
|
+
*/
|
|
65
|
+
declare function findAllMissingTranslations(registry: TranslationRegistry, specKey: string, baseLocale: Locale): Map<Locale, MissingTranslation[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Validate all translation specs in a registry.
|
|
68
|
+
*
|
|
69
|
+
* @param registry - Translation registry to validate
|
|
70
|
+
* @returns Validation result
|
|
71
|
+
*/
|
|
72
|
+
declare function validateTranslationRegistry(registry: TranslationRegistry): TranslationValidationResult;
|
|
73
|
+
declare class TranslationValidationError extends Error {
|
|
74
|
+
readonly issues: TranslationValidationIssue[];
|
|
75
|
+
constructor(message: string, issues: TranslationValidationIssue[]);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Assert that a translation spec is valid, throwing if not.
|
|
79
|
+
*
|
|
80
|
+
* @param spec - Translation spec to validate
|
|
81
|
+
* @throws {TranslationValidationError} If validation fails
|
|
82
|
+
*/
|
|
83
|
+
declare function assertTranslationSpecValid(spec: TranslationSpec): void;
|
|
84
|
+
//#endregion
|
|
85
|
+
export { ICUValidationResult, MissingTranslation, TranslationValidationError, TranslationValidationIssue, TranslationValidationLevel, TranslationValidationResult, assertTranslationSpecValid, findAllMissingTranslations, findMissingTranslations, validateICUFormat, validateTranslationRegistry, validateTranslationSpec };
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
//#region src/translations/validation.ts
|
|
2
|
+
/**
|
|
3
|
+
* Validate a translation spec for internal consistency.
|
|
4
|
+
*
|
|
5
|
+
* @param spec - Translation spec to validate
|
|
6
|
+
* @returns Validation result with any issues found
|
|
7
|
+
*/
|
|
8
|
+
function validateTranslationSpec(spec) {
|
|
9
|
+
const issues = [];
|
|
10
|
+
validateMeta(spec, issues);
|
|
11
|
+
validateLocale(spec, issues);
|
|
12
|
+
validateMessages(spec, issues);
|
|
13
|
+
validatePluralRules(spec, issues);
|
|
14
|
+
return {
|
|
15
|
+
valid: issues.filter((i) => i.level === "error").length === 0,
|
|
16
|
+
issues
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function validateMeta(spec, issues) {
|
|
20
|
+
const { meta } = spec;
|
|
21
|
+
if (!meta.key?.trim()) issues.push({
|
|
22
|
+
level: "error",
|
|
23
|
+
message: "Translation spec must have a non-empty key",
|
|
24
|
+
path: "meta.key"
|
|
25
|
+
});
|
|
26
|
+
if (!meta.version?.trim()) issues.push({
|
|
27
|
+
level: "error",
|
|
28
|
+
message: "Translation spec must have a non-empty version",
|
|
29
|
+
path: "meta.version"
|
|
30
|
+
});
|
|
31
|
+
if (!meta.domain?.trim()) issues.push({
|
|
32
|
+
level: "error",
|
|
33
|
+
message: "Translation spec must have a non-empty domain",
|
|
34
|
+
path: "meta.domain"
|
|
35
|
+
});
|
|
36
|
+
if (!meta.owners?.length) issues.push({
|
|
37
|
+
level: "warning",
|
|
38
|
+
message: "Translation spec should specify owners",
|
|
39
|
+
path: "meta.owners"
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function validateLocale(spec, issues) {
|
|
43
|
+
if (!spec.locale?.trim()) issues.push({
|
|
44
|
+
level: "error",
|
|
45
|
+
message: "Translation spec must specify a locale",
|
|
46
|
+
path: "locale"
|
|
47
|
+
});
|
|
48
|
+
else if (!isValidLocaleFormat(spec.locale)) issues.push({
|
|
49
|
+
level: "warning",
|
|
50
|
+
message: `Locale "${spec.locale}" may not be a valid ISO 639-1 code`,
|
|
51
|
+
path: "locale"
|
|
52
|
+
});
|
|
53
|
+
if (spec.fallback && !isValidLocaleFormat(spec.fallback)) issues.push({
|
|
54
|
+
level: "warning",
|
|
55
|
+
message: `Fallback locale "${spec.fallback}" may not be a valid ISO 639-1 code`,
|
|
56
|
+
path: "fallback"
|
|
57
|
+
});
|
|
58
|
+
if (spec.fallback === spec.locale) issues.push({
|
|
59
|
+
level: "warning",
|
|
60
|
+
message: "Fallback locale is the same as primary locale",
|
|
61
|
+
path: "fallback"
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function validateMessages(spec, issues) {
|
|
65
|
+
const { messages } = spec;
|
|
66
|
+
if (!messages || Object.keys(messages).length === 0) {
|
|
67
|
+
issues.push({
|
|
68
|
+
level: "warning",
|
|
69
|
+
message: "Translation spec has no messages",
|
|
70
|
+
path: "messages"
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (const [key, message] of Object.entries(messages)) {
|
|
75
|
+
const path = `messages.${key}`;
|
|
76
|
+
if (!message.value?.trim()) issues.push({
|
|
77
|
+
level: "error",
|
|
78
|
+
message: `Message "${key}" has an empty value`,
|
|
79
|
+
path: `${path}.value`
|
|
80
|
+
});
|
|
81
|
+
const valueParams = extractPlaceholders(message.value);
|
|
82
|
+
const declaredPlaceholders = message.placeholders ?? [];
|
|
83
|
+
for (const placeholder of declaredPlaceholders) if (!valueParams.includes(placeholder.name)) issues.push({
|
|
84
|
+
level: "warning",
|
|
85
|
+
message: `Placeholder "${placeholder.name}" is defined but not used in message "${key}"`,
|
|
86
|
+
path: `${path}.placeholders`
|
|
87
|
+
});
|
|
88
|
+
if (message.placeholders !== void 0) {
|
|
89
|
+
for (const param of valueParams) if (!declaredPlaceholders.find((p) => p.name === param)) issues.push({
|
|
90
|
+
level: "info",
|
|
91
|
+
message: `Placeholder "{${param}}" used in message "${key}" but not declared`,
|
|
92
|
+
path: `${path}.placeholders`
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const icuResult = validateICUFormat(message.value);
|
|
96
|
+
if (!icuResult.valid) issues.push({
|
|
97
|
+
level: "error",
|
|
98
|
+
message: `Message "${key}" has invalid ICU format: ${icuResult.error}`,
|
|
99
|
+
path: `${path}.value`
|
|
100
|
+
});
|
|
101
|
+
if (message.variants?.length) {
|
|
102
|
+
const seenVariants = /* @__PURE__ */ new Set();
|
|
103
|
+
for (const variant of message.variants) {
|
|
104
|
+
const variantKey = `${variant.type}:${variant.key}`;
|
|
105
|
+
if (seenVariants.has(variantKey)) issues.push({
|
|
106
|
+
level: "warning",
|
|
107
|
+
message: `Duplicate variant "${variantKey}" in message "${key}"`,
|
|
108
|
+
path: `${path}.variants`
|
|
109
|
+
});
|
|
110
|
+
seenVariants.add(variantKey);
|
|
111
|
+
if (!variant.value?.trim()) issues.push({
|
|
112
|
+
level: "error",
|
|
113
|
+
message: `Variant "${variantKey}" has an empty value in message "${key}"`,
|
|
114
|
+
path: `${path}.variants`
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (message.maxLength !== void 0) {
|
|
119
|
+
if (message.maxLength <= 0) issues.push({
|
|
120
|
+
level: "error",
|
|
121
|
+
message: `Message "${key}" has invalid maxLength (must be positive)`,
|
|
122
|
+
path: `${path}.maxLength`
|
|
123
|
+
});
|
|
124
|
+
else if (message.value.length > message.maxLength) issues.push({
|
|
125
|
+
level: "warning",
|
|
126
|
+
message: `Message "${key}" value exceeds maxLength (${message.value.length} > ${message.maxLength})`,
|
|
127
|
+
path: `${path}.value`
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function validatePluralRules(spec, issues) {
|
|
133
|
+
if (!spec.pluralRules?.length) return;
|
|
134
|
+
for (let i = 0; i < spec.pluralRules.length; i++) {
|
|
135
|
+
const rule = spec.pluralRules[i];
|
|
136
|
+
if (!rule) continue;
|
|
137
|
+
const path = `pluralRules[${i}]`;
|
|
138
|
+
if (!rule.variable?.trim()) issues.push({
|
|
139
|
+
level: "error",
|
|
140
|
+
message: "Plural rule must specify a variable name",
|
|
141
|
+
path: `${path}.variable`
|
|
142
|
+
});
|
|
143
|
+
if (!rule.rules?.length) issues.push({
|
|
144
|
+
level: "error",
|
|
145
|
+
message: "Plural rule must have at least one category",
|
|
146
|
+
path: `${path}.rules`
|
|
147
|
+
});
|
|
148
|
+
else if (!rule.rules.some((r) => r.category === "other")) issues.push({
|
|
149
|
+
level: "error",
|
|
150
|
+
message: `Plural rule "${rule.variable}" must have an "other" category`,
|
|
151
|
+
path: `${path}.rules`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Validate ICU message format syntax.
|
|
157
|
+
*
|
|
158
|
+
* This is a simplified validator that checks for balanced braces
|
|
159
|
+
* and extracts placeholders, plurals, and selects.
|
|
160
|
+
*
|
|
161
|
+
* @param message - Message string to validate
|
|
162
|
+
* @returns Validation result with extracted components
|
|
163
|
+
*/
|
|
164
|
+
function validateICUFormat(message) {
|
|
165
|
+
const placeholders = [];
|
|
166
|
+
const plurals = [];
|
|
167
|
+
const selects = [];
|
|
168
|
+
let braceDepth = 0;
|
|
169
|
+
let currentPlaceholder = "";
|
|
170
|
+
let inPlaceholder = false;
|
|
171
|
+
try {
|
|
172
|
+
for (let i = 0; i < message.length; i++) {
|
|
173
|
+
const char = message[i];
|
|
174
|
+
const prevChar = i > 0 ? message[i - 1] : "";
|
|
175
|
+
if (char === "'" && i + 1 < message.length) {
|
|
176
|
+
const nextChar = message[i + 1];
|
|
177
|
+
if (nextChar === "{" || nextChar === "}") {
|
|
178
|
+
i++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (char === "{") {
|
|
183
|
+
if (prevChar !== "'") {
|
|
184
|
+
braceDepth++;
|
|
185
|
+
if (braceDepth === 1) {
|
|
186
|
+
inPlaceholder = true;
|
|
187
|
+
currentPlaceholder = "";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} else if (char === "}") {
|
|
191
|
+
if (prevChar !== "'") {
|
|
192
|
+
braceDepth--;
|
|
193
|
+
if (braceDepth === 0 && inPlaceholder) {
|
|
194
|
+
processPlaceholder(currentPlaceholder.trim(), placeholders, plurals, selects);
|
|
195
|
+
inPlaceholder = false;
|
|
196
|
+
}
|
|
197
|
+
if (braceDepth < 0) return {
|
|
198
|
+
valid: false,
|
|
199
|
+
error: "Unbalanced closing brace",
|
|
200
|
+
placeholders: [],
|
|
201
|
+
plurals: [],
|
|
202
|
+
selects: []
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
} else if (inPlaceholder) currentPlaceholder += char;
|
|
206
|
+
}
|
|
207
|
+
if (braceDepth !== 0) return {
|
|
208
|
+
valid: false,
|
|
209
|
+
error: "Unbalanced opening brace",
|
|
210
|
+
placeholders: [],
|
|
211
|
+
plurals: [],
|
|
212
|
+
selects: []
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
valid: true,
|
|
216
|
+
placeholders,
|
|
217
|
+
plurals,
|
|
218
|
+
selects
|
|
219
|
+
};
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return {
|
|
222
|
+
valid: false,
|
|
223
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
224
|
+
placeholders: [],
|
|
225
|
+
plurals: [],
|
|
226
|
+
selects: []
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function processPlaceholder(placeholder, placeholders, plurals, selects) {
|
|
231
|
+
const parts = placeholder.split(",").map((p) => p.trim());
|
|
232
|
+
const name = parts[0];
|
|
233
|
+
const type = parts[1]?.toLowerCase();
|
|
234
|
+
if (name && !placeholders.includes(name)) placeholders.push(name);
|
|
235
|
+
if (type === "plural" || type === "selectordinal") {
|
|
236
|
+
if (name && !plurals.includes(name)) plurals.push(name);
|
|
237
|
+
} else if (type === "select") {
|
|
238
|
+
if (name && !selects.includes(name)) selects.push(name);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Find messages that exist in source but not in target.
|
|
243
|
+
*
|
|
244
|
+
* @param sourceSpec - Source translation spec (usually base locale like 'en')
|
|
245
|
+
* @param targetSpec - Target translation spec to check
|
|
246
|
+
* @returns Array of missing translations
|
|
247
|
+
*/
|
|
248
|
+
function findMissingTranslations(sourceSpec, targetSpec) {
|
|
249
|
+
const missing = [];
|
|
250
|
+
for (const [key, message] of Object.entries(sourceSpec.messages)) if (!targetSpec.messages[key]) missing.push({
|
|
251
|
+
messageKey: key,
|
|
252
|
+
sourceLocale: sourceSpec.locale,
|
|
253
|
+
targetLocale: targetSpec.locale,
|
|
254
|
+
sourceMessage: message
|
|
255
|
+
});
|
|
256
|
+
return missing;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Find all missing translations across a registry.
|
|
260
|
+
*
|
|
261
|
+
* Compares each locale against a base locale to find missing keys.
|
|
262
|
+
*
|
|
263
|
+
* @param registry - Translation registry
|
|
264
|
+
* @param specKey - Translation spec key to check
|
|
265
|
+
* @param baseLocale - Base locale to compare against
|
|
266
|
+
* @returns Map of locale to missing translations
|
|
267
|
+
*/
|
|
268
|
+
function findAllMissingTranslations(registry, specKey, baseLocale) {
|
|
269
|
+
const result = /* @__PURE__ */ new Map();
|
|
270
|
+
const baseSpec = registry.getLatest(specKey, baseLocale);
|
|
271
|
+
if (!baseSpec) return result;
|
|
272
|
+
const locales = registry.listLocales(specKey);
|
|
273
|
+
for (const locale of locales) {
|
|
274
|
+
if (locale === baseLocale) continue;
|
|
275
|
+
const targetSpec = registry.getLatest(specKey, locale);
|
|
276
|
+
if (targetSpec) {
|
|
277
|
+
const missing = findMissingTranslations(baseSpec, targetSpec);
|
|
278
|
+
if (missing.length > 0) result.set(locale, missing);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Validate all translation specs in a registry.
|
|
285
|
+
*
|
|
286
|
+
* @param registry - Translation registry to validate
|
|
287
|
+
* @returns Validation result
|
|
288
|
+
*/
|
|
289
|
+
function validateTranslationRegistry(registry) {
|
|
290
|
+
const issues = [];
|
|
291
|
+
for (const spec of registry.list()) {
|
|
292
|
+
const specResult = validateTranslationSpec(spec);
|
|
293
|
+
issues.push(...specResult.issues.map((i) => ({
|
|
294
|
+
...i,
|
|
295
|
+
path: `${spec.meta.key}.v${spec.meta.version}:${spec.locale}${i.path ? `.${i.path}` : ""}`
|
|
296
|
+
})));
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
valid: issues.filter((i) => i.level === "error").length === 0,
|
|
300
|
+
issues
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
var TranslationValidationError = class extends Error {
|
|
304
|
+
constructor(message, issues) {
|
|
305
|
+
super(message);
|
|
306
|
+
this.issues = issues;
|
|
307
|
+
this.name = "TranslationValidationError";
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
/**
|
|
311
|
+
* Assert that a translation spec is valid, throwing if not.
|
|
312
|
+
*
|
|
313
|
+
* @param spec - Translation spec to validate
|
|
314
|
+
* @throws {TranslationValidationError} If validation fails
|
|
315
|
+
*/
|
|
316
|
+
function assertTranslationSpecValid(spec) {
|
|
317
|
+
const result = validateTranslationSpec(spec);
|
|
318
|
+
if (!result.valid) throw new TranslationValidationError(`Translation ${spec.meta.key}.v${spec.meta.version}:${spec.locale} is invalid`, result.issues);
|
|
319
|
+
}
|
|
320
|
+
function isValidLocaleFormat(locale) {
|
|
321
|
+
return /^[a-z]{2}(-[A-Z]{2})?$/.test(locale);
|
|
322
|
+
}
|
|
323
|
+
function extractPlaceholders(value) {
|
|
324
|
+
return validateICUFormat(value).placeholders;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
//#endregion
|
|
328
|
+
export { TranslationValidationError, assertTranslationSpecValid, findAllMissingTranslations, findMissingTranslations, validateICUFormat, validateTranslationRegistry, validateTranslationSpec };
|