@vmz/vmz 0.0.0 → 0.0.2
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/README.md +48 -2
- package/bin/vmz.js +4 -0
- package/dist/application-cmd.d.ts +22 -0
- package/dist/application-cmd.js +348 -0
- package/dist/bundler-adapter.d.ts +64 -0
- package/dist/bundler-adapter.js +111 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +370 -0
- package/dist/dev-session.d.ts +35 -0
- package/dist/dev-session.js +290 -0
- package/dist/document-build.d.ts +99 -0
- package/dist/document-build.js +274 -0
- package/dist/document-check.d.ts +44 -0
- package/dist/document-check.js +246 -0
- package/dist/document-cmd.d.ts +9 -0
- package/dist/document-cmd.js +147 -0
- package/dist/document-designs.d.ts +9 -0
- package/dist/document-designs.js +126 -0
- package/dist/document-enrich.d.ts +23 -0
- package/dist/document-enrich.js +234 -0
- package/dist/document-evidence.d.ts +49 -0
- package/dist/document-evidence.js +501 -0
- package/dist/document-integrate.d.ts +35 -0
- package/dist/document-integrate.js +89 -0
- package/dist/document-interactive.d.ts +69 -0
- package/dist/document-interactive.js +255 -0
- package/dist/document-locale.d.ts +31 -0
- package/dist/document-locale.js +59 -0
- package/dist/document-markdown.d.ts +13 -0
- package/dist/document-markdown.js +39 -0
- package/dist/document-scan.d.ts +21 -0
- package/dist/document-scan.js +151 -0
- package/dist/document-schema.d.ts +87 -0
- package/dist/document-schema.js +88 -0
- package/dist/explain-cmd.d.ts +5 -0
- package/dist/explain-cmd.js +123 -0
- package/dist/index.d.ts +808 -0
- package/dist/index.js +569 -0
- package/dist/locale-check.d.ts +106 -0
- package/dist/locale-check.js +737 -0
- package/dist/locale-cmd.d.ts +5 -0
- package/dist/locale-cmd.js +443 -0
- package/dist/locale-delivery.d.ts +298 -0
- package/dist/locale-delivery.js +444 -0
- package/dist/locale-router.d.ts +207 -0
- package/dist/locale-router.js +508 -0
- package/dist/locale-runtime.d.ts +406 -0
- package/dist/locale-runtime.js +542 -0
- package/dist/locale-schema.d.ts +9 -0
- package/dist/locale-schema.js +10 -0
- package/dist/locale-tooling.d.ts +118 -0
- package/dist/locale-tooling.js +358 -0
- package/dist/log.d.ts +19 -0
- package/dist/log.js +42 -0
- package/dist/packages.d.ts +27 -0
- package/dist/packages.js +147 -0
- package/dist/plugin-host.d.ts +30 -0
- package/dist/plugin-host.js +370 -0
- package/dist/refactor-cmd.d.ts +8 -0
- package/dist/refactor-cmd.js +156 -0
- package/dist/resolve.d.ts +25 -0
- package/dist/resolve.js +56 -0
- package/dist/test-cmd.d.ts +9 -0
- package/dist/test-cmd.js +343 -0
- package/dist/test-compile.d.ts +2 -0
- package/dist/test-compile.js +3 -0
- package/dist/test-discover.d.ts +2 -0
- package/dist/test-discover.js +3 -0
- package/dist/test-logic.d.ts +2 -0
- package/dist/test-logic.js +3 -0
- package/dist/test-protocol.d.ts +2 -0
- package/dist/test-protocol.js +3 -0
- package/dist/watch-diff.d.ts +17 -0
- package/dist/watch-diff.js +56 -0
- package/package.json +86 -4
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Locale I4 multi-host delivery: Web chunks · Mini packages · Native packs ·
|
|
4
|
+
* Server error envelope / formatter resources.
|
|
5
|
+
* Design: 规划设计/vmz/28 §9–§10
|
|
6
|
+
*
|
|
7
|
+
* Same MessageNode projects to all Surfaces; LocaleId is not a WebSurface concern.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { DIAG_LOCALE_CHUNK_HASH_MISMATCH, DIAG_LOCALE_DELIVERY_FULL_BUNDLE, DIAG_LOCALE_HOST_MESSAGE_DIVERGENCE, DIAG_LOCALE_MINI_CROSS_PACKAGE_UNPROVEN, DIAG_LOCALE_NATIVE_PACK_APP_MISMATCH, DIAG_LOCALE_NATIVE_PACK_HAS_JS, DIAG_LOCALE_NATIVE_PACK_UNSIGNED, DIAG_LOCALE_SERVER_FORMAT_WITHOUT_CONTEXT, DIAG_LOCALE_SERVER_TRANSLATED_ERROR, FORMATTER_DATA_VERSION, LOCALE_CHUNK_MANIFEST_SCHEMA, LOCALE_DELIVERY_CHECK_SCHEMA, LOCALE_DELIVERY_RESOLUTION_SCHEMA, LOCALE_MINI_PACKAGE_PROOF_SCHEMA, LOCALE_NATIVE_PACK_SCHEMA, LOCALE_SERVER_ERROR_ENVELOPE_SCHEMA, } from './locale-schema.js';
|
|
11
|
+
/**
|
|
12
|
+
* @param {unknown} value
|
|
13
|
+
*/
|
|
14
|
+
function stableHash(value) {
|
|
15
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 32);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @param {Record<string, string[]>} [fallback]
|
|
19
|
+
*/
|
|
20
|
+
export function fallbackDigest(fallback = {}) {
|
|
21
|
+
const keys = Object.keys(fallback).sort();
|
|
22
|
+
const normalized = {};
|
|
23
|
+
for (const k of keys)
|
|
24
|
+
normalized[k] = [...(fallback[k] || [])];
|
|
25
|
+
return stableHash(normalized);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Hash reachable message variants for one locale.
|
|
29
|
+
* @param {Array<{ messageId: string, variants: Record<string, { template: string }> }>} messages
|
|
30
|
+
* @param {string} localeId
|
|
31
|
+
* @param {string[]} [reachableIds]
|
|
32
|
+
*/
|
|
33
|
+
export function messageCatalogHash(messages, localeId, reachableIds) {
|
|
34
|
+
const allow = reachableIds ? new Set(reachableIds) : null;
|
|
35
|
+
/** @type {Record<string, string>} */
|
|
36
|
+
const slice = {};
|
|
37
|
+
for (const m of messages || []) {
|
|
38
|
+
if (allow && !allow.has(m.messageId))
|
|
39
|
+
continue;
|
|
40
|
+
const t = m.variants?.[localeId]?.template;
|
|
41
|
+
if (t != null)
|
|
42
|
+
slice[m.messageId] = t;
|
|
43
|
+
}
|
|
44
|
+
const ordered = Object.keys(slice)
|
|
45
|
+
.sort()
|
|
46
|
+
.map((k) => [k, slice[k]]);
|
|
47
|
+
return stableHash(ordered);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build LocaleDeliveryResolution for one Host surface.
|
|
51
|
+
* @param {{
|
|
52
|
+
* host: 'web'|'mini'|'native'|'server',
|
|
53
|
+
* applicationId: string,
|
|
54
|
+
* deliveryId: string,
|
|
55
|
+
* planVersion?: string,
|
|
56
|
+
* supportedLocales: string[],
|
|
57
|
+
* defaultLocale: string,
|
|
58
|
+
* fallback?: Record<string, string[]>,
|
|
59
|
+
* routingRealization?: unknown,
|
|
60
|
+
* messages: Array<{ messageId: string, variants: Record<string, { template: string }> }>,
|
|
61
|
+
* reachableMessageIds?: string[],
|
|
62
|
+
* bundledLocales?: string[],
|
|
63
|
+
* allowFullClientBundle?: boolean,
|
|
64
|
+
* }} input
|
|
65
|
+
*/
|
|
66
|
+
export function buildLocaleDeliveryResolution(input) {
|
|
67
|
+
/** @type {any[]} */
|
|
68
|
+
const diagnostics = [];
|
|
69
|
+
const supported = [...(input.supportedLocales || [])];
|
|
70
|
+
const defaultLocale = input.defaultLocale;
|
|
71
|
+
const reachable = input.reachableMessageIds || (input.messages || []).map((m) => m.messageId);
|
|
72
|
+
const bundled = input.bundledLocales && input.bundledLocales.length ? [...input.bundledLocales] : [defaultLocale];
|
|
73
|
+
// Never default-ship every locale into a client bundle.
|
|
74
|
+
if ((input.host === 'web' || input.host === 'mini') &&
|
|
75
|
+
bundled.length >= supported.length &&
|
|
76
|
+
supported.length > 1 &&
|
|
77
|
+
!input.allowFullClientBundle) {
|
|
78
|
+
diagnostics.push({
|
|
79
|
+
code: DIAG_LOCALE_DELIVERY_FULL_BUNDLE,
|
|
80
|
+
severity: 'error',
|
|
81
|
+
message: `${input.host} delivery must not bundle all ${supported.length} locales by default`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** @type {Record<string, string>} */
|
|
85
|
+
const messageCatalogHashes = {};
|
|
86
|
+
for (const loc of supported) {
|
|
87
|
+
messageCatalogHashes[loc] = messageCatalogHash(input.messages, loc, reachable);
|
|
88
|
+
}
|
|
89
|
+
/** @type {any[]} */
|
|
90
|
+
const lazyLocaleChunks = [];
|
|
91
|
+
for (const loc of supported) {
|
|
92
|
+
if (bundled.includes(loc))
|
|
93
|
+
continue;
|
|
94
|
+
lazyLocaleChunks.push({
|
|
95
|
+
schema: LOCALE_CHUNK_MANIFEST_SCHEMA,
|
|
96
|
+
localeId: loc,
|
|
97
|
+
host: input.host,
|
|
98
|
+
messageIds: reachable,
|
|
99
|
+
hash: messageCatalogHashes[loc],
|
|
100
|
+
kind: input.host === 'mini' ? 'subpackage_module' : 'locale_chunk',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const bundledChunks = bundled.map((loc) => ({
|
|
104
|
+
schema: LOCALE_CHUNK_MANIFEST_SCHEMA,
|
|
105
|
+
localeId: loc,
|
|
106
|
+
host: input.host,
|
|
107
|
+
messageIds: reachable,
|
|
108
|
+
hash: messageCatalogHashes[loc],
|
|
109
|
+
kind: input.host === 'server' ? 'server_resources' : 'bundled',
|
|
110
|
+
}));
|
|
111
|
+
return {
|
|
112
|
+
schema: LOCALE_DELIVERY_RESOLUTION_SCHEMA,
|
|
113
|
+
status: diagnostics.length ? 'failed' : 'ready',
|
|
114
|
+
host: input.host,
|
|
115
|
+
applicationId: input.applicationId,
|
|
116
|
+
deliveryId: input.deliveryId,
|
|
117
|
+
planVersion: input.planVersion || 'plan.v0',
|
|
118
|
+
supportedLocales: supported,
|
|
119
|
+
defaultLocale,
|
|
120
|
+
fallbackDigest: fallbackDigest(input.fallback),
|
|
121
|
+
routingRealization: input.routingRealization || null,
|
|
122
|
+
bundledLocales: bundled,
|
|
123
|
+
lazyLocaleChunks,
|
|
124
|
+
bundledChunks,
|
|
125
|
+
formatterDataVersion: FORMATTER_DATA_VERSION,
|
|
126
|
+
messageCatalogHashes,
|
|
127
|
+
reachableMessageIds: reachable,
|
|
128
|
+
diagnostics,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Validate a Native optional locale pack (signed, no JS, bound to app/plan/schema).
|
|
133
|
+
* @param {{
|
|
134
|
+
* pack: {
|
|
135
|
+
* schema?: string,
|
|
136
|
+
* applicationId: string,
|
|
137
|
+
* planVersion: string,
|
|
138
|
+
* localeId: string,
|
|
139
|
+
* signature?: string,
|
|
140
|
+
* catalog?: Record<string, string>,
|
|
141
|
+
* formatterDataVersion?: string,
|
|
142
|
+
* entries?: Array<{ path: string, kind?: string }>,
|
|
143
|
+
* executable?: boolean,
|
|
144
|
+
* },
|
|
145
|
+
* expectedApplicationId: string,
|
|
146
|
+
* expectedPlanVersion: string,
|
|
147
|
+
* }} input
|
|
148
|
+
*/
|
|
149
|
+
export function validateNativeLocalePack(input) {
|
|
150
|
+
/** @type {any[]} */
|
|
151
|
+
const diagnostics = [];
|
|
152
|
+
const pack = input.pack || {};
|
|
153
|
+
if (!pack.signature) {
|
|
154
|
+
diagnostics.push({
|
|
155
|
+
code: DIAG_LOCALE_NATIVE_PACK_UNSIGNED,
|
|
156
|
+
severity: 'error',
|
|
157
|
+
message: 'Native locale pack must be signed',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (pack.applicationId !== input.expectedApplicationId) {
|
|
161
|
+
diagnostics.push({
|
|
162
|
+
code: DIAG_LOCALE_NATIVE_PACK_APP_MISMATCH,
|
|
163
|
+
severity: 'error',
|
|
164
|
+
message: `pack applicationId ${pack.applicationId} != ${input.expectedApplicationId}`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (pack.planVersion !== input.expectedPlanVersion) {
|
|
168
|
+
diagnostics.push({
|
|
169
|
+
code: DIAG_LOCALE_NATIVE_PACK_APP_MISMATCH,
|
|
170
|
+
severity: 'error',
|
|
171
|
+
message: `pack planVersion ${pack.planVersion} != ${input.expectedPlanVersion}`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (pack.executable === true) {
|
|
175
|
+
diagnostics.push({
|
|
176
|
+
code: DIAG_LOCALE_NATIVE_PACK_HAS_JS,
|
|
177
|
+
severity: 'error',
|
|
178
|
+
message: 'Native locale pack must not carry executable JavaScript',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
for (const e of pack.entries || []) {
|
|
182
|
+
const path = String(e.path || '');
|
|
183
|
+
const kind = String(e.kind || '');
|
|
184
|
+
if (/\.(js|mjs|cjs|wasm)$/i.test(path) || kind === 'javascript' || kind === 'script') {
|
|
185
|
+
diagnostics.push({
|
|
186
|
+
code: DIAG_LOCALE_NATIVE_PACK_HAS_JS,
|
|
187
|
+
severity: 'error',
|
|
188
|
+
message: `Native pack entry forbids executable ${path || kind}`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (pack.formatterDataVersion && pack.formatterDataVersion !== FORMATTER_DATA_VERSION) {
|
|
193
|
+
diagnostics.push({
|
|
194
|
+
code: DIAG_LOCALE_CHUNK_HASH_MISMATCH,
|
|
195
|
+
severity: 'error',
|
|
196
|
+
message: `formatterDataVersion ${pack.formatterDataVersion} != ${FORMATTER_DATA_VERSION}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
schema: LOCALE_NATIVE_PACK_SCHEMA,
|
|
201
|
+
status: diagnostics.length ? 'failed' : 'ready',
|
|
202
|
+
localeId: pack.localeId,
|
|
203
|
+
diagnostics,
|
|
204
|
+
pack: {
|
|
205
|
+
schema: LOCALE_NATIVE_PACK_SCHEMA,
|
|
206
|
+
applicationId: pack.applicationId,
|
|
207
|
+
planVersion: pack.planVersion,
|
|
208
|
+
localeId: pack.localeId,
|
|
209
|
+
signature: pack.signature || null,
|
|
210
|
+
formatterDataVersion: pack.formatterDataVersion || FORMATTER_DATA_VERSION,
|
|
211
|
+
catalogKeys: Object.keys(pack.catalog || {}).sort(),
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Mini cross-subpackage message dependencies must be proven.
|
|
217
|
+
* @param {{
|
|
218
|
+
* packages: Array<{ id: string, messageIds: string[] }>,
|
|
219
|
+
* edges: Array<{ fromPackage: string, toPackage: string, messageId: string }>,
|
|
220
|
+
* }} input
|
|
221
|
+
*/
|
|
222
|
+
export function proveMiniPackageMessages(input) {
|
|
223
|
+
/** @type {any[]} */
|
|
224
|
+
const diagnostics = [];
|
|
225
|
+
/** @type {Map<string, Set<string>>} */
|
|
226
|
+
const owned = new Map();
|
|
227
|
+
for (const p of input.packages || []) {
|
|
228
|
+
owned.set(p.id, new Set(p.messageIds || []));
|
|
229
|
+
}
|
|
230
|
+
/** @type {any[]} */
|
|
231
|
+
const proven = [];
|
|
232
|
+
for (const edge of input.edges || []) {
|
|
233
|
+
const target = owned.get(edge.toPackage);
|
|
234
|
+
if (!target || !target.has(edge.messageId)) {
|
|
235
|
+
diagnostics.push({
|
|
236
|
+
code: DIAG_LOCALE_MINI_CROSS_PACKAGE_UNPROVEN,
|
|
237
|
+
severity: 'error',
|
|
238
|
+
message: `cross-package message ${edge.messageId}: ${edge.fromPackage} -> ${edge.toPackage} unproven`,
|
|
239
|
+
});
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
proven.push({
|
|
243
|
+
fromPackage: edge.fromPackage,
|
|
244
|
+
toPackage: edge.toPackage,
|
|
245
|
+
messageId: edge.messageId,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
schema: LOCALE_MINI_PACKAGE_PROOF_SCHEMA,
|
|
250
|
+
status: diagnostics.length ? 'failed' : 'ready',
|
|
251
|
+
proven,
|
|
252
|
+
diagnostics,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Server/client boundary: ErrorCode + params only (no translated strings).
|
|
257
|
+
* @param {any} payload
|
|
258
|
+
*/
|
|
259
|
+
export function assertServerErrorEnvelope(payload) {
|
|
260
|
+
/** @type {any[]} */
|
|
261
|
+
const diagnostics = [];
|
|
262
|
+
if (payload == null || typeof payload !== 'object') {
|
|
263
|
+
diagnostics.push({
|
|
264
|
+
code: DIAG_LOCALE_SERVER_TRANSLATED_ERROR,
|
|
265
|
+
severity: 'error',
|
|
266
|
+
message: 'server error envelope must be an object',
|
|
267
|
+
});
|
|
268
|
+
return { schema: LOCALE_SERVER_ERROR_ENVELOPE_SCHEMA, status: 'failed', diagnostics };
|
|
269
|
+
}
|
|
270
|
+
if (typeof payload.message === 'string' || typeof payload.text === 'string' || typeof payload.localized === 'string') {
|
|
271
|
+
diagnostics.push({
|
|
272
|
+
code: DIAG_LOCALE_SERVER_TRANSLATED_ERROR,
|
|
273
|
+
severity: 'error',
|
|
274
|
+
message: 'server must not cross boundary with translated error strings',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (typeof payload.code !== 'string' || !payload.code) {
|
|
278
|
+
diagnostics.push({
|
|
279
|
+
code: DIAG_LOCALE_SERVER_TRANSLATED_ERROR,
|
|
280
|
+
severity: 'error',
|
|
281
|
+
message: 'server error envelope requires stable ErrorCode `code`',
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
schema: LOCALE_SERVER_ERROR_ENVELOPE_SCHEMA,
|
|
286
|
+
status: diagnostics.length ? 'failed' : 'ready',
|
|
287
|
+
envelope: {
|
|
288
|
+
schema: LOCALE_SERVER_ERROR_ENVELOPE_SCHEMA,
|
|
289
|
+
code: payload.code,
|
|
290
|
+
params: payload.params || {},
|
|
291
|
+
},
|
|
292
|
+
diagnostics,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Final-user server formatting (mail/export/push) requires explicit LocaleContext.
|
|
297
|
+
* @param {{ localeContext?: { localeId?: string, timeZone?: string }|null, purpose: string }} input
|
|
298
|
+
*/
|
|
299
|
+
export function assertServerFormatContext(input) {
|
|
300
|
+
/** @type {any[]} */
|
|
301
|
+
const diagnostics = [];
|
|
302
|
+
const ctx = input.localeContext;
|
|
303
|
+
if (!ctx?.localeId || !ctx?.timeZone) {
|
|
304
|
+
diagnostics.push({
|
|
305
|
+
code: DIAG_LOCALE_SERVER_FORMAT_WITHOUT_CONTEXT,
|
|
306
|
+
severity: 'error',
|
|
307
|
+
message: `server ${input.purpose} formatting requires explicit LocaleContext`,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return { ok: diagnostics.length === 0, diagnostics };
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Same MessageId / catalog hashes must agree across Host projections.
|
|
314
|
+
* @param {Array<ReturnType<typeof buildLocaleDeliveryResolution>>} resolutions
|
|
315
|
+
*/
|
|
316
|
+
export function assertHostMessageInvariant(resolutions) {
|
|
317
|
+
/** @type {any[]} */
|
|
318
|
+
const diagnostics = [];
|
|
319
|
+
if (!resolutions?.length)
|
|
320
|
+
return { ok: true, diagnostics };
|
|
321
|
+
const base = resolutions[0];
|
|
322
|
+
for (const r of resolutions.slice(1)) {
|
|
323
|
+
for (const loc of base.supportedLocales || []) {
|
|
324
|
+
if (base.messageCatalogHashes[loc] !== r.messageCatalogHashes?.[loc]) {
|
|
325
|
+
diagnostics.push({
|
|
326
|
+
code: DIAG_LOCALE_HOST_MESSAGE_DIVERGENCE,
|
|
327
|
+
severity: 'error',
|
|
328
|
+
message: `messageCatalogHashes[${loc}] diverge between ${base.host} and ${r.host}`,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (base.formatterDataVersion !== r.formatterDataVersion) {
|
|
333
|
+
diagnostics.push({
|
|
334
|
+
code: DIAG_LOCALE_HOST_MESSAGE_DIVERGENCE,
|
|
335
|
+
severity: 'error',
|
|
336
|
+
message: `formatterDataVersion diverge between ${base.host} and ${r.host}`,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (base.fallbackDigest !== r.fallbackDigest) {
|
|
340
|
+
diagnostics.push({
|
|
341
|
+
code: DIAG_LOCALE_HOST_MESSAGE_DIVERGENCE,
|
|
342
|
+
severity: 'error',
|
|
343
|
+
message: `fallbackDigest diverge between ${base.host} and ${r.host}`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return { ok: diagnostics.length === 0, diagnostics };
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Aggregate I4 delivery proof for fixture / CLI.
|
|
351
|
+
* @param {{
|
|
352
|
+
* manifest: {
|
|
353
|
+
* defaultLocale: string,
|
|
354
|
+
* locales: Array<{ id: string }>,
|
|
355
|
+
* fallback?: Record<string, string[]>,
|
|
356
|
+
* routing?: unknown,
|
|
357
|
+
* },
|
|
358
|
+
* messages: Array<{ messageId: string, variants: Record<string, { template: string }> }>,
|
|
359
|
+
* applicationId?: string,
|
|
360
|
+
* planVersion?: string,
|
|
361
|
+
* reachableMessageIds?: string[],
|
|
362
|
+
* }} input
|
|
363
|
+
*/
|
|
364
|
+
export function checkLocaleDelivery(input) {
|
|
365
|
+
/** @type {any[]} */
|
|
366
|
+
const diagnostics = [];
|
|
367
|
+
const supported = (input.manifest?.locales || []).map((l) => l.id);
|
|
368
|
+
const defaultLocale = input.manifest?.defaultLocale;
|
|
369
|
+
const messages = input.messages || [];
|
|
370
|
+
const reachable = input.reachableMessageIds || messages.map((m) => m.messageId);
|
|
371
|
+
const common = {
|
|
372
|
+
applicationId: input.applicationId || 'app.locales-fixture',
|
|
373
|
+
deliveryId: 'delivery.multi',
|
|
374
|
+
planVersion: input.planVersion || 'plan.v0',
|
|
375
|
+
supportedLocales: supported,
|
|
376
|
+
defaultLocale,
|
|
377
|
+
fallback: input.manifest?.fallback || {},
|
|
378
|
+
routingRealization: input.manifest?.routing || null,
|
|
379
|
+
messages,
|
|
380
|
+
reachableMessageIds: reachable,
|
|
381
|
+
bundledLocales: [defaultLocale],
|
|
382
|
+
};
|
|
383
|
+
const web = buildLocaleDeliveryResolution({ ...common, host: 'web', deliveryId: 'delivery.web' });
|
|
384
|
+
const mini = buildLocaleDeliveryResolution({ ...common, host: 'mini', deliveryId: 'delivery.mini' });
|
|
385
|
+
const native = buildLocaleDeliveryResolution({
|
|
386
|
+
...common,
|
|
387
|
+
host: 'native',
|
|
388
|
+
deliveryId: 'delivery.native',
|
|
389
|
+
});
|
|
390
|
+
const server = buildLocaleDeliveryResolution({
|
|
391
|
+
...common,
|
|
392
|
+
host: 'server',
|
|
393
|
+
deliveryId: 'delivery.server',
|
|
394
|
+
// server may include all reachable locales as resources (not a client bundle)
|
|
395
|
+
bundledLocales: supported,
|
|
396
|
+
allowFullClientBundle: true,
|
|
397
|
+
});
|
|
398
|
+
for (const r of [web, mini, native, server])
|
|
399
|
+
diagnostics.push(...r.diagnostics);
|
|
400
|
+
const invariant = assertHostMessageInvariant([web, mini, native, server]);
|
|
401
|
+
diagnostics.push(...invariant.diagnostics);
|
|
402
|
+
const packOk = validateNativeLocalePack({
|
|
403
|
+
pack: {
|
|
404
|
+
applicationId: common.applicationId,
|
|
405
|
+
planVersion: common.planVersion,
|
|
406
|
+
localeId: 'en-us',
|
|
407
|
+
signature: 'sig.test',
|
|
408
|
+
catalog: { 'account.actions.save': 'Save' },
|
|
409
|
+
formatterDataVersion: FORMATTER_DATA_VERSION,
|
|
410
|
+
entries: [{ path: 'catalog.json5', kind: 'catalog' }],
|
|
411
|
+
},
|
|
412
|
+
expectedApplicationId: common.applicationId,
|
|
413
|
+
expectedPlanVersion: common.planVersion,
|
|
414
|
+
});
|
|
415
|
+
// Probe only — do not fail delivery check if packOk ready.
|
|
416
|
+
if (packOk.status !== 'ready')
|
|
417
|
+
diagnostics.push(...packOk.diagnostics);
|
|
418
|
+
const miniProof = proveMiniPackageMessages({
|
|
419
|
+
packages: [
|
|
420
|
+
{ id: 'main', messageIds: ['account.actions.save'] },
|
|
421
|
+
{ id: 'pkg-account', messageIds: ['account.greeting', 'account.itemCount'] },
|
|
422
|
+
],
|
|
423
|
+
edges: [{ fromPackage: 'main', toPackage: 'pkg-account', messageId: 'account.greeting' }],
|
|
424
|
+
});
|
|
425
|
+
if (miniProof.status !== 'ready')
|
|
426
|
+
diagnostics.push(...miniProof.diagnostics);
|
|
427
|
+
const errOk = assertServerErrorEnvelope({
|
|
428
|
+
code: 'account.email_taken',
|
|
429
|
+
params: { email: 'a@b.c' },
|
|
430
|
+
});
|
|
431
|
+
if (errOk.status !== 'ready')
|
|
432
|
+
diagnostics.push(...errOk.diagnostics);
|
|
433
|
+
const hasErrors = diagnostics.some((d) => d.severity === 'error');
|
|
434
|
+
return {
|
|
435
|
+
schema: LOCALE_DELIVERY_CHECK_SCHEMA,
|
|
436
|
+
status: hasErrors ? 'failed' : 'ready',
|
|
437
|
+
resolutions: { web, mini, native, server },
|
|
438
|
+
nativePackProbe: packOk,
|
|
439
|
+
miniPackageProof: miniProof,
|
|
440
|
+
serverErrorEnvelope: errOk,
|
|
441
|
+
formatterDataVersion: FORMATTER_DATA_VERSION,
|
|
442
|
+
diagnostics,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale I3 router / PageMeta: route realization · canonical · hreflang ·
|
|
3
|
+
* Link locale retain · locale-aware cache key.
|
|
4
|
+
* Design: 规划设计/vmz/28 §6 · §10 · 20
|
|
5
|
+
*
|
|
6
|
+
* LocaleId is a RouteNode realization dimension — not part of stable RouteId.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Join locale prefix with a stable route path pattern.
|
|
10
|
+
* @param {string} localeId
|
|
11
|
+
* @param {string} pathPattern stable path without locale (e.g. /account/profile)
|
|
12
|
+
* @param {{ strategy?: string, defaultPrefix?: string, defaultLocale?: string }} routing
|
|
13
|
+
*/
|
|
14
|
+
export declare function realizeRoutePath(localeId: any, pathPattern: any, routing?: {}): {
|
|
15
|
+
schema: string;
|
|
16
|
+
localeId: any;
|
|
17
|
+
path: string;
|
|
18
|
+
prefixed: boolean;
|
|
19
|
+
strategy: any;
|
|
20
|
+
defaultPrefix?: undefined;
|
|
21
|
+
} | {
|
|
22
|
+
schema: string;
|
|
23
|
+
localeId: any;
|
|
24
|
+
path: string;
|
|
25
|
+
prefixed: boolean;
|
|
26
|
+
strategy: string;
|
|
27
|
+
defaultPrefix: any;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Build RouteId × LocaleId realization table.
|
|
31
|
+
* @param {{
|
|
32
|
+
* routes: Array<{ routeId: string, path: string }>,
|
|
33
|
+
* locales: string[],
|
|
34
|
+
* defaultLocale: string,
|
|
35
|
+
* routing?: { strategy?: string, defaultPrefix?: string },
|
|
36
|
+
* }} input
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildLocaleRouteRealizationTable(input: any): {
|
|
39
|
+
schema: string;
|
|
40
|
+
status: string;
|
|
41
|
+
routing: {
|
|
42
|
+
strategy: any;
|
|
43
|
+
defaultPrefix: any;
|
|
44
|
+
defaultLocale: any;
|
|
45
|
+
};
|
|
46
|
+
realizations: any[];
|
|
47
|
+
diagnostics: any[];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Absolute URL helper.
|
|
51
|
+
* @param {string} origin
|
|
52
|
+
* @param {string} path
|
|
53
|
+
*/
|
|
54
|
+
export declare function absoluteUrl(origin: any, path: any): string;
|
|
55
|
+
/**
|
|
56
|
+
* Locale-aware PageMeta for one RouteId × LocaleId.
|
|
57
|
+
* @param {{
|
|
58
|
+
* routeId: string,
|
|
59
|
+
* localeId: string,
|
|
60
|
+
* direction?: string,
|
|
61
|
+
* title: string,
|
|
62
|
+
* description?: string,
|
|
63
|
+
* origin: string,
|
|
64
|
+
* realizations: Array<{ routeId: string, localeId: string, path: string }>,
|
|
65
|
+
* locales: string[],
|
|
66
|
+
* defaultLocale: string,
|
|
67
|
+
* }} input
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildLocalePageMeta(input: any): {
|
|
70
|
+
schema: string;
|
|
71
|
+
routeId: any;
|
|
72
|
+
locale: any;
|
|
73
|
+
htmlLang: any;
|
|
74
|
+
dir: any;
|
|
75
|
+
title: any;
|
|
76
|
+
description: any;
|
|
77
|
+
canonical: string;
|
|
78
|
+
alternates: any[];
|
|
79
|
+
diagnostics: any[];
|
|
80
|
+
status: string;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* `<Link to="routeId">` retains current locale — never hand-written localized paths.
|
|
84
|
+
* @param {{
|
|
85
|
+
* to: string,
|
|
86
|
+
* currentLocale: string,
|
|
87
|
+
* realizations: Array<{ routeId: string, localeId: string, path: string }>,
|
|
88
|
+
* }} input
|
|
89
|
+
*/
|
|
90
|
+
export declare function resolveLinkHref(input: any): {
|
|
91
|
+
schema: string;
|
|
92
|
+
status: string;
|
|
93
|
+
to: any;
|
|
94
|
+
href: any;
|
|
95
|
+
localeId: any;
|
|
96
|
+
diagnostics: {
|
|
97
|
+
code: string;
|
|
98
|
+
severity: string;
|
|
99
|
+
message: string;
|
|
100
|
+
}[];
|
|
101
|
+
routeId?: undefined;
|
|
102
|
+
} | {
|
|
103
|
+
schema: string;
|
|
104
|
+
status: string;
|
|
105
|
+
to: any;
|
|
106
|
+
routeId: any;
|
|
107
|
+
localeId: any;
|
|
108
|
+
href: any;
|
|
109
|
+
diagnostics: any[];
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Parse locale prefix from a pathname under prefix strategy.
|
|
113
|
+
* @param {string} pathname
|
|
114
|
+
* @param {string[]} supportedLocales
|
|
115
|
+
*/
|
|
116
|
+
export declare function parseLocaleFromPath(pathname: any, supportedLocales: any): {
|
|
117
|
+
localeId: string;
|
|
118
|
+
restPath: string;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Plan redirect / negotiation for an incoming URL (omit-prefix aware).
|
|
122
|
+
* @param {{
|
|
123
|
+
* pathname: string,
|
|
124
|
+
* supportedLocales: string[],
|
|
125
|
+
* defaultLocale: string,
|
|
126
|
+
* routing?: { strategy?: string, defaultPrefix?: string },
|
|
127
|
+
* hostCandidates?: string[],
|
|
128
|
+
* preference?: string|null,
|
|
129
|
+
* userChoice?: string|null,
|
|
130
|
+
* }} input
|
|
131
|
+
*/
|
|
132
|
+
export declare function planLocalePathNavigation(input: any): {
|
|
133
|
+
schema: string;
|
|
134
|
+
kind: string;
|
|
135
|
+
pathname: string;
|
|
136
|
+
routeLocale: string;
|
|
137
|
+
negotiatedLocale: any;
|
|
138
|
+
restPath: string;
|
|
139
|
+
redirectTo: any;
|
|
140
|
+
realizedPath: string;
|
|
141
|
+
diagnostics: any[];
|
|
142
|
+
contentLocale: any;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Locale-aware cache key — must include LocaleId so Accept-Language cannot steal content.
|
|
146
|
+
* @param {{ routeId: string, localeId: string, path: string }} input
|
|
147
|
+
*/
|
|
148
|
+
export declare function localeAwareCacheKey(input: any): string;
|
|
149
|
+
/**
|
|
150
|
+
* Reject Vary: Accept-Language on a cache key that does not encode LocaleId.
|
|
151
|
+
* @param {{ cacheKey: string, varyAcceptLanguage?: boolean, localeId?: string }} input
|
|
152
|
+
*/
|
|
153
|
+
export declare function assertLocaleCacheKey(input: any): {
|
|
154
|
+
ok: boolean;
|
|
155
|
+
diagnostics: any[];
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* LocaleTransition must commit Route realization + PageMeta together.
|
|
159
|
+
* @param {{
|
|
160
|
+
* fromLocale: string,
|
|
161
|
+
* toLocale: string,
|
|
162
|
+
* routeId: string,
|
|
163
|
+
* realizations: Array<{ routeId: string, localeId: string, path: string }>,
|
|
164
|
+
* pageMetaByLocale: Record<string, { locale: string, canonical: string }>,
|
|
165
|
+
* }} input
|
|
166
|
+
*/
|
|
167
|
+
export declare function commitLocaleRouteMetaTransition(input: any): {
|
|
168
|
+
schema: string;
|
|
169
|
+
kind: string;
|
|
170
|
+
status: string;
|
|
171
|
+
fromLocale: any;
|
|
172
|
+
toLocale: any;
|
|
173
|
+
fromPath: any;
|
|
174
|
+
toPath: any;
|
|
175
|
+
pageMeta: any;
|
|
176
|
+
diagnostics: any[];
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Aggregate I3 router/meta proof.
|
|
180
|
+
* @param {{
|
|
181
|
+
* manifest: {
|
|
182
|
+
* defaultLocale: string,
|
|
183
|
+
* locales: Array<{ id: string, direction?: string }>,
|
|
184
|
+
* routing?: { strategy?: string, defaultPrefix?: string },
|
|
185
|
+
* },
|
|
186
|
+
* routes: Array<{ routeId: string, path: string }>,
|
|
187
|
+
* titles?: Record<string, Record<string, string>>,
|
|
188
|
+
* origin?: string,
|
|
189
|
+
* }} input
|
|
190
|
+
*/
|
|
191
|
+
export declare function checkLocaleRouter(input: any): {
|
|
192
|
+
schema: string;
|
|
193
|
+
status: string;
|
|
194
|
+
realizationTable: {
|
|
195
|
+
schema: string;
|
|
196
|
+
status: string;
|
|
197
|
+
routing: {
|
|
198
|
+
strategy: any;
|
|
199
|
+
defaultPrefix: any;
|
|
200
|
+
defaultLocale: any;
|
|
201
|
+
};
|
|
202
|
+
realizations: any[];
|
|
203
|
+
diagnostics: any[];
|
|
204
|
+
};
|
|
205
|
+
pageMetas: any[];
|
|
206
|
+
diagnostics: any[];
|
|
207
|
+
};
|