@valbuild/shared 0.126.0 → 0.127.0

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.
@@ -1125,6 +1125,16 @@ var SerializedCodeSchema = zod.z.object(getSitemapTree._objectSpread2(getSitemap
1125
1125
  options: CodeOptions.optional(),
1126
1126
  opt: zod.z["boolean"]()
1127
1127
  }));
1128
+ var SerializedLocaleSchema = zod.z.object({
1129
+ type: zod.z.literal("locale"),
1130
+ render: FieldRender,
1131
+ preview: zod.z.literal(true).optional(),
1132
+ opt: zod.z["boolean"](),
1133
+ customValidate: zod.z["boolean"]().optional(),
1134
+ readonly: zod.z["boolean"]().optional(),
1135
+ hidden: zod.z["boolean"]().optional(),
1136
+ description: zod.z.string().optional()
1137
+ });
1128
1138
  var SerializedRouteSchema = zod.z.object(getSitemapTree._objectSpread2(getSitemapTree._objectSpread2({}, commonSchemaFields), {}, {
1129
1139
  type: zod.z.literal("route"),
1130
1140
  options: zod.z.object({
@@ -1150,10 +1160,15 @@ var SerializedSettingsSchema = zod.z.lazy(function () {
1150
1160
  return zod.z.object(getSitemapTree._objectSpread2(getSitemapTree._objectSpread2({}, commonSchemaFields), {}, {
1151
1161
  type: zod.z.literal("settings"),
1152
1162
  items: zod.z.record(zod.z.string(), SerializedSchema),
1153
- opt: zod.z["boolean"]()
1163
+ opt: zod.z["boolean"](),
1164
+ // Which section this is, where Val has rules about it beyond its keys.
1165
+ // Dropping it would leave the Studio's validation worker running every
1166
+ // field's own rules and none of the cross-field ones — see
1167
+ // `sectionValidators` in core/src/schema/settings.ts.
1168
+ section: zod.z.string().optional()
1154
1169
  }));
1155
1170
  });
1156
- var SerializedSchema = zod.z.union([SerializedStringSchema, SerializedLiteralSchema, SerializedBooleanSchema, SerializedNumberSchema, SerializedObjectSchema, SerializedArraySchema, SerializedDiscriminatedUnionSchema, SerializedEnumSchema, SerializedRichTextSchema, SerializedRecordSchema, SerializedKeyOfSchema, SerializedRouteSchema, SerializedFileSchema, SerializedDateSchema, SerializedDateTimeSchema, SerializedColorSchema, SerializedCodeSchema, SerializedSettingsSchema, SerializedImageSchema]);
1171
+ var SerializedSchema = zod.z.union([SerializedStringSchema, SerializedLiteralSchema, SerializedBooleanSchema, SerializedNumberSchema, SerializedObjectSchema, SerializedArraySchema, SerializedDiscriminatedUnionSchema, SerializedEnumSchema, SerializedRichTextSchema, SerializedRecordSchema, SerializedKeyOfSchema, SerializedRouteSchema, SerializedLocaleSchema, SerializedFileSchema, SerializedDateSchema, SerializedDateTimeSchema, SerializedColorSchema, SerializedCodeSchema, SerializedSettingsSchema, SerializedImageSchema]);
1157
1172
 
1158
1173
  var ValCommit = zod.z.object({
1159
1174
  commitSha: zod.z.string(),
@@ -1329,7 +1344,7 @@ var ValConfig = zod.z.object({
1329
1344
  gitCommit: zod.z.string().optional(),
1330
1345
  gitBranch: zod.z.string().optional()
1331
1346
  });
1332
- var ValidationFixZ = zod.z.union([zod.z.literal("image:add-metadata"), zod.z.literal("image:check-metadata"), zod.z.literal("image:check-remote"), zod.z.literal("image:upload-remote"), zod.z.literal("image:download-remote"), zod.z.literal("images:check-remote"), zod.z.literal("images:upload-remote"), zod.z.literal("file:add-metadata"), zod.z.literal("file:check-metadata"), zod.z.literal("file:check-remote"), zod.z.literal("file:upload-remote"), zod.z.literal("file:download-remote"), zod.z.literal("files:check-remote"), zod.z.literal("files:upload-remote"), zod.z.literal("keyof:check-keys"), zod.z.literal("router:check-route"), zod.z.literal("images:check-unique-folder"), zod.z.literal("files:check-unique-folder"), zod.z.literal("images:check-all-files"), zod.z.literal("files:check-all-files"), zod.z.literal("jsonValues:extract-entry")]);
1347
+ var ValidationFixZ = zod.z.union([zod.z.literal("image:add-metadata"), zod.z.literal("image:check-metadata"), zod.z.literal("image:check-remote"), zod.z.literal("image:upload-remote"), zod.z.literal("image:download-remote"), zod.z.literal("images:check-remote"), zod.z.literal("images:upload-remote"), zod.z.literal("file:add-metadata"), zod.z.literal("file:check-metadata"), zod.z.literal("file:check-remote"), zod.z.literal("file:upload-remote"), zod.z.literal("file:download-remote"), zod.z.literal("files:check-remote"), zod.z.literal("files:upload-remote"), zod.z.literal("keyof:check-keys"), zod.z.literal("router:check-route"), zod.z.literal("locale:check-locale"), zod.z.literal("record:fill-keys"), zod.z.literal("images:check-unique-folder"), zod.z.literal("files:check-unique-folder"), zod.z.literal("images:check-all-files"), zod.z.literal("files:check-all-files"), zod.z.literal("jsonValues:extract-entry")]);
1333
1348
  var ValidationError = zod.z.object({
1334
1349
  message: zod.z.string(),
1335
1350
  value: zod.z.unknown().optional(),
@@ -3236,7 +3251,7 @@ var compatible = {
3236
3251
  * `readonly Json[]` — so the check that reads naturally does not type. This
3237
3252
  * predicate is that check, said once.
3238
3253
  */
3239
- function isJsonObject(value) {
3254
+ function isJsonObject$1(value) {
3240
3255
  return getSitemapTree._typeof(value) === "object" && value !== null && !Array.isArray(value);
3241
3256
  }
3242
3257
  function incompatible(reason) {
@@ -3293,7 +3308,7 @@ function variantOf(schema, value) {
3293
3308
  return undefined;
3294
3309
  }
3295
3310
  var discriminator = schema.key;
3296
- if (!isJsonObject(value)) {
3311
+ if (!isJsonObject$1(value)) {
3297
3312
  return undefined;
3298
3313
  }
3299
3314
  var tag = value[discriminator];
@@ -3421,7 +3436,7 @@ function checkCompatibility(from, to) {
3421
3436
  }
3422
3437
  case "object":
3423
3438
  {
3424
- if (from.schema.type !== "object" || !isJsonObject(from.value)) {
3439
+ if (from.schema.type !== "object" || !isJsonObject$1(from.value)) {
3425
3440
  return {
3426
3441
  status: "unknown",
3427
3442
  message: "The old value is not shaped like an object."
@@ -3524,7 +3539,7 @@ function checkCompatibility(from, to) {
3524
3539
  }
3525
3540
  case "record":
3526
3541
  {
3527
- if (from.schema.type !== "record" || !isJsonObject(from.value)) {
3542
+ if (from.schema.type !== "record" || !isJsonObject$1(from.value)) {
3528
3543
  return {
3529
3544
  status: "unknown",
3530
3545
  message: "The old value is not a record."
@@ -3960,6 +3975,59 @@ function checkRouteIsValid(route, includePattern, excludePattern, snapshot) {
3960
3975
  error: false
3961
3976
  };
3962
3977
  }
3978
+
3979
+ /**
3980
+ * The languages the project declares, from its settings module.
3981
+ *
3982
+ * Empty where there is no settings module, no `locales` section, or nothing in
3983
+ * it — all of which mean the same thing: this project has not said it is
3984
+ * translated.
3985
+ */
3986
+ function declaredLocales(snapshot) {
3987
+ var _resolveSettingsModul = core.resolveSettingsModule(snapshot.schemas),
3988
+ moduleFilePath = _resolveSettingsModul.moduleFilePath;
3989
+ if (moduleFilePath === null) {
3990
+ return [];
3991
+ }
3992
+ var source = snapshot.sources[moduleFilePath];
3993
+ if (getSitemapTree._typeof(source) !== "object" || source === null || Array.isArray(source)) {
3994
+ return [];
3995
+ }
3996
+ var locales = source["locales"];
3997
+ if (getSitemapTree._typeof(locales) !== "object" || locales === null || Array.isArray(locales)) {
3998
+ return [];
3999
+ }
4000
+ var available = locales["available"];
4001
+ if (!Array.isArray(available)) {
4002
+ return [];
4003
+ }
4004
+ return available.filter(function (tag) {
4005
+ return typeof tag === "string";
4006
+ });
4007
+ }
4008
+ /**
4009
+ * The fixes {@link resolveSchemaSourceFixForError} answers for.
4010
+ *
4011
+ * A marker error carrying one of these is NOT a message: it says "ask the whole
4012
+ * project about this" and carries the value to ask with, because the schema
4013
+ * that raised it can only see its own module. Left unresolved, what a user sees
4014
+ * is the marker's own placeholder text — "should typically be processed by Val
4015
+ * internally … version mismatch" — which is alarming and says nothing.
4016
+ *
4017
+ * A list rather than a check per call site: every caller that resolves some of
4018
+ * these has to resolve all of them, and the two that leaked did so by adding a
4019
+ * fix here and not to a condition somewhere else.
4020
+ */
4021
+ var SCHEMA_SOURCE_FIXES = ["keyof:check-keys", "router:check-route", "locale:check-locale", "record:fill-keys"];
4022
+
4023
+ /** Whether an error is a marker only the whole project can answer. */
4024
+ function isSchemaSourceFixError(error) {
4025
+ var _error$fixes;
4026
+ return ((_error$fixes = error.fixes) !== null && _error$fixes !== void 0 ? _error$fixes : []).some(function (fix) {
4027
+ return SCHEMA_SOURCE_FIXES.includes(fix);
4028
+ });
4029
+ }
4030
+
3963
4031
  /**
3964
4032
  * The record a `keyof:check-keys` error resolves against, or null for any other
3965
4033
  * error — including a malformed marker, which
@@ -3971,8 +4039,8 @@ function checkRouteIsValid(route, includePattern, excludePattern, snapshot) {
3971
4039
  * to the referenced record can invalidate the module holding the reference.
3972
4040
  */
3973
4041
  function keyOfRecordPath(error) {
3974
- var _error$fixes;
3975
- if (!((_error$fixes = error.fixes) !== null && _error$fixes !== void 0 ? _error$fixes : []).includes("keyof:check-keys")) {
4042
+ var _error$fixes2;
4043
+ if (!((_error$fixes2 = error.fixes) !== null && _error$fixes2 !== void 0 ? _error$fixes2 : []).includes("keyof:check-keys")) {
3976
4044
  return null;
3977
4045
  }
3978
4046
  var value = error.value;
@@ -3984,15 +4052,15 @@ function keyOfRecordPath(error) {
3984
4052
  }
3985
4053
 
3986
4054
  /**
3987
- * Resolves a single `keyof:check-keys` or `router:check-route` error against
3988
- * the given schema/source snapshot. Returns:
4055
+ * Resolves a single marker error (see {@link SCHEMA_SOURCE_FIXES}) against the
4056
+ * given schema/source snapshot. Returns:
3989
4057
  * - null if the error is not one of these fixes (caller should pass through)
3990
- * - { status: "resolved" } if the referenced key/route is valid (drop error)
4058
+ * - { status: "resolved" } if the reference is valid (drop error)
3991
4059
  * - { status: "remaining"; error } if invalid (rewritten error to surface)
3992
4060
  */
3993
4061
  function resolveSchemaSourceFixForError(error, snapshot) {
3994
- var _error$fixes2;
3995
- var fixes = (_error$fixes2 = error.fixes) !== null && _error$fixes2 !== void 0 ? _error$fixes2 : [];
4062
+ var _error$fixes3;
4063
+ var fixes = (_error$fixes3 = error.fixes) !== null && _error$fixes3 !== void 0 ? _error$fixes3 : [];
3996
4064
  if (fixes.includes("keyof:check-keys")) {
3997
4065
  if (!error.value) {
3998
4066
  return {
@@ -4051,6 +4119,120 @@ function resolveSchemaSourceFixForError(error, snapshot) {
4051
4119
  status: "resolved"
4052
4120
  };
4053
4121
  }
4122
+ if (fixes.includes("locale:check-locale")) {
4123
+ var value = error.value;
4124
+ if (getSitemapTree._typeof(value) !== "object" || value === null) {
4125
+ return {
4126
+ status: "remaining",
4127
+ error: getSitemapTree._objectSpread2(getSitemapTree._objectSpread2({}, error), {}, {
4128
+ message: "Expected locale validation error to have a 'value' object. Found: ".concat(getSitemapTree._typeof(value), ". ").concat(TYPE_ERROR_MESSAGE),
4129
+ typeError: true,
4130
+ fixes: undefined
4131
+ })
4132
+ };
4133
+ }
4134
+ var _ref6 = value,
4135
+ locale = _ref6.locale;
4136
+ if (typeof locale !== "string") {
4137
+ return {
4138
+ status: "remaining",
4139
+ error: getSitemapTree._objectSpread2(getSitemapTree._objectSpread2({}, error), {}, {
4140
+ message: "Expected locale validation error 'value' to have property 'locale' of type 'string'. Found: ".concat(getSitemapTree._typeof(locale), ". ").concat(TYPE_ERROR_MESSAGE),
4141
+ typeError: true,
4142
+ fixes: undefined
4143
+ })
4144
+ };
4145
+ }
4146
+ var available = declaredLocales(snapshot);
4147
+ if (available.length === 0) {
4148
+ return {
4149
+ status: "remaining",
4150
+ error: getSitemapTree._objectSpread2(getSitemapTree._objectSpread2({}, error), {}, {
4151
+ // The project has not said it is translated, so the value is not
4152
+ // wrong so much as premature — and the fix is in another file.
4153
+ message: "This project has no languages. Declare them under 'locales.available' in the settings module before using s.locale().",
4154
+ fixes: undefined
4155
+ })
4156
+ };
4157
+ }
4158
+ if (available.includes(locale)) {
4159
+ return {
4160
+ status: "resolved"
4161
+ };
4162
+ }
4163
+ return {
4164
+ status: "remaining",
4165
+ error: getSitemapTree._objectSpread2(getSitemapTree._objectSpread2({}, error), {}, {
4166
+ message: "'".concat(locale, "' is not one of this project's languages: ").concat(available.map(function (each) {
4167
+ return "'".concat(each, "'");
4168
+ }).join(", ")),
4169
+ fixes: undefined
4170
+ })
4171
+ };
4172
+ }
4173
+ if (fixes.includes("record:fill-keys")) {
4174
+ var _value = error.value;
4175
+ if (getSitemapTree._typeof(_value) !== "object" || _value === null) {
4176
+ return {
4177
+ status: "remaining",
4178
+ error: getSitemapTree._objectSpread2(getSitemapTree._objectSpread2({}, error), {}, {
4179
+ message: "Expected record key validation error to have a 'value' object. Found: ".concat(getSitemapTree._typeof(_value), ". ").concat(TYPE_ERROR_MESSAGE),
4180
+ typeError: true,
4181
+ fixes: undefined
4182
+ })
4183
+ };
4184
+ }
4185
+ var _ref7 = _value,
4186
+ present = _ref7.present,
4187
+ declared = _ref7.declared;
4188
+ if (!Array.isArray(present)) {
4189
+ return {
4190
+ status: "remaining",
4191
+ error: getSitemapTree._objectSpread2(getSitemapTree._objectSpread2({}, error), {}, {
4192
+ message: "Expected record key validation error 'value' to have property 'present' of type 'array'. Found: ".concat(getSitemapTree._typeof(present), ". ").concat(TYPE_ERROR_MESSAGE),
4193
+ typeError: true,
4194
+ fixes: undefined
4195
+ })
4196
+ };
4197
+ }
4198
+ var presentKeys = present.filter(function (key) {
4199
+ return typeof key === "string";
4200
+ });
4201
+ // A literal union brought its keys along; a locale record left them to be
4202
+ // read out of the settings module, which is the whole reason this check is
4203
+ // resolved here rather than in the schema.
4204
+ var declaredKeys = Array.isArray(declared) ? declared.filter(function (key) {
4205
+ return typeof key === "string";
4206
+ }) : declaredLocales(snapshot);
4207
+ if (declaredKeys.length === 0) {
4208
+ // A locale record on a project that has declared no languages. There is
4209
+ // nothing to require, and the locale field's own check already says the
4210
+ // project has not declared any — saying it twice on one record would not
4211
+ // add anything.
4212
+ return {
4213
+ status: "resolved"
4214
+ };
4215
+ }
4216
+ var missing = core.missingDeclaredKeys(declaredKeys, presentKeys);
4217
+ if (missing.length === 0) {
4218
+ return {
4219
+ status: "resolved"
4220
+ };
4221
+ }
4222
+ return {
4223
+ status: "remaining",
4224
+ error: getSitemapTree._objectSpread2(getSitemapTree._objectSpread2({}, error), {}, {
4225
+ message: "Missing ".concat(missing.length === 1 ? "key" : "keys", ": ").concat(missing.map(function (each) {
4226
+ return "'".concat(each, "'");
4227
+ }).join(", "), ". This record's keys are declared by its schema, so every one of them is an entry \u2014 an entry nobody has written yet is null, not absent."),
4228
+ value: {
4229
+ missing: missing,
4230
+ declared: declaredKeys
4231
+ },
4232
+ fixes: undefined
4233
+ })
4234
+ };
4235
+ }
4054
4236
  if (fixes.includes("router:check-route")) {
4055
4237
  if (!error.value) {
4056
4238
  return {
@@ -4112,10 +4294,10 @@ function asRegExpPattern(value) {
4112
4294
  /**
4113
4295
  * Apply schema/source fix resolution across a map of validation errors.
4114
4296
  *
4115
- * Errors with `keyof:check-keys` or `router:check-route` are resolved against
4116
- * the in-memory schema/source snapshot — valid references drop the error,
4117
- * invalid ones are rewritten with a "did you mean…" message and have their
4118
- * `fixes` cleared (so downstream code reports them as plain validation errors).
4297
+ * Errors carrying one of {@link SCHEMA_SOURCE_FIXES} are resolved against the
4298
+ * in-memory schema/source snapshot — valid references drop the error, invalid
4299
+ * ones are rewritten with a "did you mean…" message and have their `fixes`
4300
+ * cleared (so downstream code reports them as plain validation errors).
4119
4301
  * All other errors pass through untouched.
4120
4302
  *
4121
4303
  * Source paths with no remaining errors are removed from the result.
@@ -4149,6 +4331,189 @@ function resolveSchemaSourceFixes(validationErrors, snapshot) {
4149
4331
  return result;
4150
4332
  }
4151
4333
 
4334
+ /**
4335
+ * Which of the project's languages governs a path, or `null` for none.
4336
+ *
4337
+ * THE question the whole feature turns on. A locale scope is a subtree in one
4338
+ * language, and three things open one: a `locale` field on an object, a record
4339
+ * keyed by `s.locale()`, and a locale segment in a segmented key. Once this can
4340
+ * be answered from the schema and the source alone, everything downstream falls
4341
+ * out of it — the Studio's locale filter, deep links, `<html lang>`, and knowing
4342
+ * what to translate from and into.
4343
+ *
4344
+ * One implementation, so the Studio, the server and the validation worker cannot
4345
+ * disagree about what language a piece of content is in.
4346
+ *
4347
+ * `null` means no scope governs the path — which is the answer for most content
4348
+ * in most projects, and for every project that has not declared any languages.
4349
+ */
4350
+ function localeAt(path, snapshot) {
4351
+ var available = declaredLocales(snapshot);
4352
+ if (available.length === 0) {
4353
+ // Nothing can be one of no languages. Answering `null` rather than reading
4354
+ // the content is also what makes this cheap for the projects that are not
4355
+ // translated at all.
4356
+ return null;
4357
+ }
4358
+ var _Internal$splitModule = core.Internal.splitModuleFilePathAndModulePath(path),
4359
+ _Internal$splitModule2 = _slicedToArray(_Internal$splitModule, 2),
4360
+ moduleFilePath = _Internal$splitModule2[0],
4361
+ modulePath = _Internal$splitModule2[1];
4362
+ var schema = snapshot.schemas[moduleFilePath];
4363
+ var source = snapshot.sources[moduleFilePath];
4364
+ if (schema === undefined || source === undefined) {
4365
+ return null;
4366
+ }
4367
+ var segments = modulePath ? core.Internal.splitModulePath(modulePath) : [];
4368
+ return walk(schema, source, segments, available);
4369
+ }
4370
+
4371
+ /**
4372
+ * Walk from a node towards `segments`, answering with the innermost scope.
4373
+ *
4374
+ * The scope is read on ARRIVAL at each node — the node the path names included,
4375
+ * not only the ones passed through on the way to it. So `localeAt` of a
4376
+ * scope-opening object is that object's own locale rather than its parent's,
4377
+ * and a path that stops AT a block still answers with the block's language.
4378
+ *
4379
+ * A scope may not contain another (see `localeScopeErrors`), so at most one of
4380
+ * these can fire on a well-formed path; taking the innermost is what makes the
4381
+ * answer well-defined while a project is mid-fix.
4382
+ */
4383
+ function walk(schema, source, segments, available) {
4384
+ var entered = enter(schema, source, available);
4385
+ var locale = entered.locale;
4386
+ var currentSchema = entered.schema;
4387
+ var currentSource = source;
4388
+ var _iterator = getSitemapTree._createForOfIteratorHelper(segments),
4389
+ _step;
4390
+ try {
4391
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
4392
+ var segment = _step.value;
4393
+ if (currentSchema === undefined) {
4394
+ return locale;
4395
+ }
4396
+ if (currentSchema.type === "record") {
4397
+ var _currentSchema$key;
4398
+ if (((_currentSchema$key = currentSchema.key) === null || _currentSchema$key === void 0 ? void 0 : _currentSchema$key.type) === "locale") {
4399
+ // In a locale-keyed record the KEY is the language, so the segment we
4400
+ // are about to take is the answer.
4401
+ var resolved = core.localeOfValue(segment, available);
4402
+ if (resolved !== null) {
4403
+ locale = resolved;
4404
+ }
4405
+ }
4406
+ currentSchema = currentSchema.item;
4407
+ } else if (currentSchema.type === "array") {
4408
+ currentSchema = currentSchema.item;
4409
+ } else if (currentSchema.type === "object" || currentSchema.type === "settings") {
4410
+ currentSchema = currentSchema.items[segment];
4411
+ } else {
4412
+ // A leaf, or richtext's internal structure. Nothing below opens a scope.
4413
+ return locale;
4414
+ }
4415
+ currentSource = childSource(currentSource, segment);
4416
+ entered = enter(currentSchema, currentSource, available);
4417
+ currentSchema = entered.schema;
4418
+ if (entered.locale !== null) {
4419
+ locale = entered.locale;
4420
+ }
4421
+ }
4422
+ } catch (err) {
4423
+ _iterator.e(err);
4424
+ } finally {
4425
+ _iterator.f();
4426
+ }
4427
+ return locale;
4428
+ }
4429
+
4430
+ /**
4431
+ * Arrive at a node: resolve what it really is, and read the scope it opens.
4432
+ *
4433
+ * A discriminated union is a fork rather than a level — the variant the value
4434
+ * takes IS the node — so it is resolved here, before anything asks what the
4435
+ * node holds. Doing that on arrival rather than on the way down is the
4436
+ * difference between a block's own path answering with its language and
4437
+ * answering with nothing.
4438
+ */
4439
+ function enter(schema, source, available) {
4440
+ var resolved = (schema === null || schema === void 0 ? void 0 : schema.type) === "discriminated-union" ? variantOfUnion(schema, source) : schema;
4441
+ return {
4442
+ schema: resolved,
4443
+ locale: resolved === undefined ? null : localeOfObjectField(resolved, source, available)
4444
+ };
4445
+ }
4446
+
4447
+ /**
4448
+ * The language an object's own `locale` field says it is in, if it has one.
4449
+ *
4450
+ * Reads the SOURCE, since unlike a record key the value is content rather than
4451
+ * part of the path. A field that has not been filled in, or holds something
4452
+ * that is not one of the project's languages, is not an answer — validation is
4453
+ * already reporting that, and guessing here would put a language in
4454
+ * `<html lang>` that nobody chose.
4455
+ */
4456
+ function localeOfObjectField(schema, source, available) {
4457
+ if (schema.type !== "object" || !isJsonObject(source)) {
4458
+ return null;
4459
+ }
4460
+ for (var _i = 0, _Object$entries = Object.entries(schema.items); _i < _Object$entries.length; _i++) {
4461
+ var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
4462
+ key = _Object$entries$_i[0],
4463
+ item = _Object$entries$_i[1];
4464
+ if (item.type !== "locale") {
4465
+ continue;
4466
+ }
4467
+ var value = source[key];
4468
+ if (typeof value !== "string") {
4469
+ return null;
4470
+ }
4471
+ return core.localeOfValue(value, available);
4472
+ }
4473
+ return null;
4474
+ }
4475
+
4476
+ /**
4477
+ * The variant a value takes, or `undefined` if the tag matches none.
4478
+ *
4479
+ * The tag is read off the source here; picking the variant is
4480
+ * {@link discriminatedUnionBranchOf}, shared with the Studio's locale filter so
4481
+ * the two cannot disagree about which variant a row is. `s.enum()` never
4482
+ * reaches this — it is a leaf, with nothing under it to walk into.
4483
+ */
4484
+ function variantOfUnion(schema, source) {
4485
+ if (!isJsonObject(source)) {
4486
+ return undefined;
4487
+ }
4488
+ return core.discriminatedUnionBranchOf(schema, source[schema.key]);
4489
+ }
4490
+
4491
+ /** The child of a source value at a segment, or `null` where there is none. */
4492
+ function childSource(source, segment) {
4493
+ var _source$segment;
4494
+ if (Array.isArray(source)) {
4495
+ var _source$index;
4496
+ var index = Number(segment);
4497
+ return Number.isInteger(index) ? (_source$index = source[index]) !== null && _source$index !== void 0 ? _source$index : null : null;
4498
+ }
4499
+ if (!isJsonObject(source)) {
4500
+ return null;
4501
+ }
4502
+ return (_source$segment = source[segment]) !== null && _source$segment !== void 0 ? _source$segment : null;
4503
+ }
4504
+
4505
+ /**
4506
+ * Whether `source` is a JSON object rather than an array or a primitive.
4507
+ *
4508
+ * A type predicate rather than the inline checks, because `Array.isArray` does
4509
+ * NOT narrow `JsonArray` out of `Json` — it is `readonly Json[]`, and the
4510
+ * built-in guard only narrows mutable arrays. Without this, every read of a
4511
+ * property would need an assertion.
4512
+ */
4513
+ function isJsonObject(source) {
4514
+ return getSitemapTree._typeof(source) === "object" && source !== null && !Array.isArray(source);
4515
+ }
4516
+
4152
4517
  /**
4153
4518
  * Local `yyyy-MM-dd`, which is the one thing this module used `date-fns` for.
4154
4519
  *
@@ -4185,15 +4550,27 @@ function clampDateTimeString(value, options) {
4185
4550
  }
4186
4551
  return value;
4187
4552
  }
4188
- function emptyOf(schema) {
4553
+
4554
+ /**
4555
+ * What `emptyOf` cannot read off a serialized schema.
4556
+ *
4557
+ * The project's languages, and which one is being worked in. Neither is in the
4558
+ * schema: a locale-keyed record has one entry per language and the languages
4559
+ * are declared in the settings module, and "which language am I writing" is a
4560
+ * fact about the editor, not about the content. Optional throughout: a caller
4561
+ * that has no context gets an empty record and an unset locale, which
4562
+ * validation then reports, rather than wrong ones.
4563
+ */
4564
+
4565
+ function emptyOf(schema, context) {
4189
4566
  if (schema.type === "object") {
4190
4567
  return Object.fromEntries(Object.keys(schema.items).map(function (key) {
4191
- return [key, emptyOf(schema.items[key])];
4568
+ return [key, emptyOf(schema.items[key], context)];
4192
4569
  }));
4193
4570
  } else if (schema.type === "array") {
4194
4571
  return [];
4195
4572
  } else if (schema.type === "record") {
4196
- return {};
4573
+ return emptyRecord(schema, context);
4197
4574
  } else if (schema.type === "settings") {
4198
4575
  // Not an object of empty sections: every settings key is optional, and
4199
4576
  // absent IS the empty value. Filling the sections in would write a shape
@@ -4217,6 +4594,16 @@ function emptyOf(schema) {
4217
4594
  }
4218
4595
  } else if (schema.type === "route") {
4219
4596
  return ""; // Empty string as default route value
4597
+ } else if (schema.type === "locale") {
4598
+ var _context$selectedLoca;
4599
+ // Which languages exist is in the settings module, which `emptyOf` has no
4600
+ // access to — it works from a serialized schema alone. So a caller that
4601
+ // has narrowed to one language says so and gets it; a caller that has not
4602
+ // gets the empty string, which is not a language and which validation
4603
+ // reports. Guessing `locales[0]` instead would file content under a
4604
+ // language nobody chose, which is the thing this whole feature exists to
4605
+ // make visible.
4606
+ return (_context$selectedLoca = context === null || context === void 0 ? void 0 : context.selectedLocale) !== null && _context$selectedLoca !== void 0 ? _context$selectedLoca : "";
4220
4607
  } else if (schema.type === "file" || schema.type === "image") {
4221
4608
  return null; // returning null is the only thing we can do, however, it means that the patches cannot be applied yet since that might fail
4222
4609
  } else if (schema.type === "literal") {
@@ -4229,7 +4616,7 @@ function emptyOf(schema) {
4229
4616
  if (schema.items.length === 0) {
4230
4617
  throw Error("Cannot create an empty value for a discriminated union with no variants");
4231
4618
  }
4232
- return emptyOf(schema.items[0]);
4619
+ return emptyOf(schema.items[0], context);
4233
4620
  } else if (schema.type === "enum") {
4234
4621
  if (schema.values.length === 0) {
4235
4622
  throw Error("Cannot create an empty value for an enum with no values");
@@ -4255,6 +4642,32 @@ function emptyOf(schema) {
4255
4642
  throw Error("Unexpected schema type: " + JSON.stringify(_exhaustiveCheck));
4256
4643
  }
4257
4644
 
4645
+ /**
4646
+ * An empty record — with every key its schema declares already in it.
4647
+ *
4648
+ * An open record (`s.record(s.string(), item)`) starts empty, because there is
4649
+ * no key anyone could mean. A record whose key schema enumerates its keys is
4650
+ * the opposite: the keys are part of the schema, so an empty one is already
4651
+ * missing them, and handing back `{}` would create content that fails
4652
+ * validation the moment it is written.
4653
+ *
4654
+ * The entries are `null` rather than `emptyOf(item)`. A null entry reads as "not
4655
+ * filled in yet", which is what a language nobody has translated into IS —
4656
+ * whereas an entry of empty strings claims someone wrote it and left it blank,
4657
+ * and would count as translated in every list and filter downstream.
4658
+ */
4659
+ function emptyRecord(schema, context) {
4660
+ var _context$locales;
4661
+ var declared = core.declaredKeySetOf(schema.key);
4662
+ if (declared === null) {
4663
+ return {};
4664
+ }
4665
+ var keys = declared.kind === "literals" ? declared.keys : (_context$locales = context === null || context === void 0 ? void 0 : context.locales) !== null && _context$locales !== void 0 ? _context$locales : [];
4666
+ return Object.fromEntries(keys.map(function (key) {
4667
+ return [key, null];
4668
+ }));
4669
+ }
4670
+
4258
4671
  var toolNames = ["get_all_schema", "get_source", "search_content", "validate_content", "create_patch", "add_session_image_to_gallery", "remove_image_gallery_entry", "navigate_to", "get_current_context", "get_patches", "get_source_path_from_route", "set_session_name", "show_compare_view", "ask_user_question", "duplicate_source", "empty_at_path", "count_entries", "get_record_keys"];
4259
4672
 
4260
4673
  function buildFileRefValue(filePath, metadata) {
@@ -4631,7 +5044,13 @@ function buildEmptyAtPathPatch(args, moduleSchema,
4631
5044
  // Required, not optional: the occupied-key check below is the only thing
4632
5045
  // standing between "scaffold an entry" and "delete the entry that was there",
4633
5046
  // and an optional argument is one a caller forgets.
4634
- moduleSource) {
5047
+ moduleSource,
5048
+ // The project's languages, for a locale-keyed record: its entries ARE the
5049
+ // languages, and they are declared in another module, so an empty one built
5050
+ // without them is `{}` — which the completeness rule then reports as every
5051
+ // language missing. Both callers can answer: the Studio from the context, the
5052
+ // MCP tools from the snapshot they already hold. See `EmptyOfContext`.
5053
+ context) {
4635
5054
  var destinationSchema;
4636
5055
  if (args.destinationPath.length === 0) {
4637
5056
  destinationSchema = moduleSchema;
@@ -4677,7 +5096,7 @@ moduleSource) {
4677
5096
  if (occupied) {
4678
5097
  return occupied;
4679
5098
  }
4680
- var value = emptyOf(destinationSchema);
5099
+ var value = emptyOf(destinationSchema, context);
4681
5100
  return safeParsePatch([{
4682
5101
  op: decision.op,
4683
5102
  path: args.destinationPath,
@@ -4868,6 +5287,8 @@ function isSkippableFixCode(fix) {
4868
5287
  return true;
4869
5288
  case "keyof:check-keys":
4870
5289
  case "router:check-route":
5290
+ case "locale:check-locale":
5291
+ case "record:fill-keys":
4871
5292
  case "jsonValues:extract-entry":
4872
5293
  // `jsonValues:extract-entry` is an entry inlined in the `.val.ts`. The
4873
5294
  // browser cannot fix it (that means writing a new `*.val.json` and
@@ -5275,8 +5696,8 @@ function traverseSchemaSource(source, schema, path, callback) {
5275
5696
  return;
5276
5697
  }
5277
5698
 
5278
- // Handle route
5279
- if (schema.type === "route") {
5699
+ // Handle route and locale: both are plain strings at a leaf
5700
+ if (schema.type === "route" || schema.type === "locale") {
5280
5701
  if (typeof source === "string") {
5281
5702
  callback({
5282
5703
  source: source,
@@ -5503,7 +5924,13 @@ function indexModule(searchIndex, moduleFilePath, source, schema) {
5503
5924
  // An enum's value is a string the editor chose; it is as searchable as
5504
5925
  // any other string leaf. The old string union was never indexed at all,
5505
5926
  // which is the gap this closes.
5506
- schema.type === "enum") {
5927
+ schema.type === "enum" ||
5928
+ // A locale tag, for the same reason: it is what the editor picked and
5929
+ // what they see in the field, so searching "nb-NO" has to find the
5930
+ // Norwegian rows. `traverseSchemaSource` visits these, and visiting a
5931
+ // leaf only to index it as the empty string is worse than not visiting
5932
+ // it — it looks like content that does not exist.
5933
+ schema.type === "locale") {
5507
5934
  var _source$toString, _source$toString2;
5508
5935
  searchText = (_source$toString = source === null || source === void 0 ? void 0 : source.toString()) !== null && _source$toString !== void 0 ? _source$toString : "";
5509
5936
  label = (_source$toString2 = source === null || source === void 0 ? void 0 : source.toString()) !== null && _source$toString2 !== void 0 ? _source$toString2 : "";
@@ -5698,6 +6125,7 @@ exports.RemirrorText = RemirrorText;
5698
6125
  exports.RemirrorTextMark = RemirrorTextMark;
5699
6126
  exports.RestoreVerdict = RestoreVerdict;
5700
6127
  exports.RichTextOptions = RichTextOptions;
6128
+ exports.SCHEMA_SOURCE_FIXES = SCHEMA_SOURCE_FIXES;
5701
6129
  exports.SerializedArraySchema = SerializedArraySchema;
5702
6130
  exports.SerializedBooleanSchema = SerializedBooleanSchema;
5703
6131
  exports.SerializedCodeSchema = SerializedCodeSchema;
@@ -5710,6 +6138,7 @@ exports.SerializedFileSchema = SerializedFileSchema;
5710
6138
  exports.SerializedImageSchema = SerializedImageSchema;
5711
6139
  exports.SerializedKeyOfSchema = SerializedKeyOfSchema;
5712
6140
  exports.SerializedLiteralSchema = SerializedLiteralSchema;
6141
+ exports.SerializedLocaleSchema = SerializedLocaleSchema;
5713
6142
  exports.SerializedNumberSchema = SerializedNumberSchema;
5714
6143
  exports.SerializedObjectSchema = SerializedObjectSchema;
5715
6144
  exports.SerializedRecordSchema = SerializedRecordSchema;
@@ -5736,6 +6165,7 @@ exports.concatModulePath = concatModulePath;
5736
6165
  exports.createRegExpFromPattern = createRegExpFromPattern;
5737
6166
  exports.createSearchIndex = createSearchIndex;
5738
6167
  exports.createValClient = createValClient;
6168
+ exports.declaredLocales = declaredLocales;
5739
6169
  exports.describeContainerAtPath = describeContainerAtPath;
5740
6170
  exports.emptyOf = emptyOf;
5741
6171
  exports.explainIncompatible = explainIncompatible;
@@ -5755,9 +6185,11 @@ exports.getSourceAt = getSourceAt;
5755
6185
  exports.indexModule = indexModule;
5756
6186
  exports.isMimeTypeAccepted = isMimeTypeAccepted;
5757
6187
  exports.isRemoteSchema = isRemoteSchema;
6188
+ exports.isSchemaSourceFixError = isSchemaSourceFixError;
5758
6189
  exports.isSkippedSource = isSkippedSource;
5759
6190
  exports.keyOfRecordPath = keyOfRecordPath;
5760
6191
  exports.levenshtein = levenshtein;
6192
+ exports.localeAt = localeAt;
5761
6193
  exports.newestCommitSha = newestCommitSha;
5762
6194
  exports.partitionValidationErrors = partitionValidationErrors;
5763
6195
  exports.performSearch = performSearch;