@form-engine-ts/core 4.6.0 → 4.8.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.
package/README.md CHANGED
@@ -52,6 +52,9 @@ Required locales cover every source text that exists on the form, its fields, op
52
52
  safe maximum-length corrections while purging unregistered locale content. Pass
53
53
  `{ policy: { allowedLocales, maxLocales } }` to `populateSchemaTranslations` to reject inadmissible targets before the
54
54
  translation adapter runs.
55
+ `normalizeLocale(locale)` returns the canonical BCP 47 tag, accepts underscore-separated compatibility input such as
56
+ `ja_JP`, and returns `null` for invalid tags. Schema validation, sanitization, locale policy checks, and translation
57
+ slot lookup use this same normalization so equivalent locale spellings cannot bypass constraints.
55
58
 
56
59
  Fields can use a `displayRule` with nested `all`/`any` condition groups and `show` or `hide` actions. Supported
57
60
  operators include equality, containment, emptiness, and numeric comparisons; the legacy `displayCondition` and
@@ -63,7 +66,9 @@ and missing, translated, stale, or manual states for authoring tools. `populateS
63
66
  and missing entries while preserving manual translations and reports skipped reasons.
64
67
 
65
68
  Legacy translation metadata can be recognized with `isManualTranslationMetadata` and migrated with
66
- `migrateSchemaTranslationMetadata`; pass a custom migrator when legacy fields need application-specific conversion.
69
+ `migrateSchemaTranslationMetadata`; pass a custom migrator (directly or as `{ migrator }`) when legacy fields need
70
+ application-specific conversion. The migrator receives `TranslationMigrationContext` with the locale, JSON path,
71
+ property, node kind, and node identifiers.
67
72
  `PopulateTranslationsOptions` is a compatibility alias for `PopulateTranslationOptions`, which also accepts custom
68
73
  manual-translation detection and metadata normalization callbacks. `removeLocaleFromSchema(schema, locale)` removes
69
74
  the locale from registrations and all form, page, field, option translation values and metadata; the default locale
package/dist/index.cjs CHANGED
@@ -57,6 +57,7 @@ __export(index_exports, {
57
57
  matchesSubmissionFilter: () => matchesSubmissionFilter,
58
58
  matchesSubmissionPageFilters: () => matchesSubmissionPageFilters,
59
59
  migrateSchemaTranslationMetadata: () => migrateSchemaTranslationMetadata,
60
+ normalizeLocale: () => normalizeLocale,
60
61
  normalizeSubmissionPageSize: () => normalizeSubmissionPageSize,
61
62
  pipeResponsesToCsvStream: () => pipeResponsesToCsvStream,
62
63
  populateSchemaTranslations: () => populateSchemaTranslations,
@@ -74,11 +75,27 @@ __export(index_exports, {
74
75
  });
75
76
  module.exports = __toCommonJS(index_exports);
76
77
 
78
+ // src/locale.ts
79
+ var normalizeLocale = (rawLocale) => {
80
+ if (!rawLocale || typeof rawLocale !== "string") return null;
81
+ const trimmed = rawLocale.trim().replace(/_/gu, "-");
82
+ if (trimmed.length === 0) return null;
83
+ try {
84
+ return Intl.getCanonicalLocales(trimmed)[0] ?? null;
85
+ } catch {
86
+ return null;
87
+ }
88
+ };
89
+ function canonicalLocaleOrRaw(rawLocale) {
90
+ return normalizeLocale(rawLocale) ?? rawLocale;
91
+ }
92
+
77
93
  // src/policy.ts
78
94
  function collectRecordKeys(value, path, pathsByLocale) {
79
- for (const locale of Object.keys(value ?? {})) {
95
+ for (const rawLocale of Object.keys(value ?? {})) {
96
+ const locale = canonicalLocaleOrRaw(rawLocale);
80
97
  const paths = pathsByLocale.get(locale) ?? [];
81
- paths.push(`${path}.${locale}`);
98
+ paths.push(`${path}.${rawLocale}`);
82
99
  pathsByLocale.set(locale, paths);
83
100
  }
84
101
  }
@@ -109,13 +126,15 @@ function collectSchemaLocales(schema) {
109
126
  });
110
127
  const translationLocales = new Set(pathsByLocale.keys());
111
128
  const allUniqueLocales = /* @__PURE__ */ new Set([
112
- ...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
113
- ...schema.supportedLocales ?? [],
129
+ ...schema.defaultLocale === void 0 ? [] : [canonicalLocaleOrRaw(schema.defaultLocale)],
130
+ ...(schema.supportedLocales ?? []).map(canonicalLocaleOrRaw),
114
131
  ...translationLocales
115
132
  ]);
133
+ const defaultLocale = schema.defaultLocale === void 0 ? void 0 : canonicalLocaleOrRaw(schema.defaultLocale);
134
+ const supportedLocales = (schema.supportedLocales ?? []).map(canonicalLocaleOrRaw);
116
135
  return {
117
- ...schema.defaultLocale === void 0 ? {} : { defaultLocale: schema.defaultLocale },
118
- supportedLocales: schema.supportedLocales ?? [],
136
+ ...defaultLocale === void 0 ? {} : { defaultLocale },
137
+ supportedLocales,
119
138
  translationLocales,
120
139
  allUniqueLocales,
121
140
  translationLocalePaths: pathsByLocale
@@ -138,7 +157,12 @@ function displayRuleSourceIds(field) {
138
157
  }
139
158
  function registeredEntries(value, registeredLocales) {
140
159
  if (value === void 0) return void 0;
141
- const entries = Object.entries(value).filter(([locale]) => registeredLocales.has(locale));
160
+ const entries = [];
161
+ for (const [rawLocale, entry] of Object.entries(value)) {
162
+ const locale = normalizeLocale(rawLocale);
163
+ if (locale === null || !registeredLocales.has(locale)) continue;
164
+ entries.push([locale, entry]);
165
+ }
142
166
  return entries.length === 0 ? void 0 : Object.fromEntries(entries);
143
167
  }
144
168
  function sanitizeNodeLocales(node, registeredLocales) {
@@ -176,6 +200,16 @@ function sanitizePageLocales(page, registeredLocales) {
176
200
  const translations = registeredEntries(page.translations, registeredLocales);
177
201
  return { ...base, ...translations === void 0 ? {} : { translations } };
178
202
  }
203
+ function normalizedLocaleList(locales) {
204
+ return [
205
+ ...new Set(
206
+ locales.flatMap((locale) => {
207
+ const normalized = normalizeLocale(locale);
208
+ return normalized === null ? [] : [normalized];
209
+ })
210
+ )
211
+ ];
212
+ }
179
213
  function sanitizeFieldConstraints(field, policy) {
180
214
  const constraint = policy?.fieldConstraints?.[field.type];
181
215
  if (constraint === void 0) return field;
@@ -290,10 +324,9 @@ function validateSchemaStructure(schema) {
290
324
  }
291
325
  function sanitizeSchema(schema, options = {}) {
292
326
  const existingQuestionIds = new Set(schema.fields.map((field) => field.id));
293
- const registeredLocales = /* @__PURE__ */ new Set([
294
- ...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
295
- ...schema.supportedLocales ?? []
296
- ]);
327
+ const defaultLocale = schema.defaultLocale === void 0 ? null : normalizeLocale(schema.defaultLocale);
328
+ const supportedLocales = schema.supportedLocales === void 0 ? void 0 : normalizedLocaleList(schema.supportedLocales);
329
+ const registeredLocales = /* @__PURE__ */ new Set([...defaultLocale === null ? [] : [defaultLocale], ...supportedLocales ?? []]);
297
330
  const cyclic = cyclicQuestionIds(schema.fields);
298
331
  const sanitizedFields = schema.fields.map((sourceField) => {
299
332
  const field = sanitizeFieldConstraints(sanitizeFieldLocales(sourceField, registeredLocales), options.policy);
@@ -311,10 +344,17 @@ function sanitizeSchema(schema, options = {}) {
311
344
  return sanitized;
312
345
  });
313
346
  const localizedSchema = sanitizeNodeLocales(schema, registeredLocales);
314
- const { translations: _translations, ...schemaWithoutLocaleContent } = localizedSchema;
347
+ const {
348
+ defaultLocale: _defaultLocale,
349
+ supportedLocales: _supportedLocales,
350
+ translations: _translations,
351
+ ...schemaWithoutLocaleContent
352
+ } = localizedSchema;
315
353
  const translations = registeredEntries(schema.translations, registeredLocales);
316
354
  const base = {
317
355
  ...schemaWithoutLocaleContent,
356
+ ...defaultLocale === null ? {} : { defaultLocale },
357
+ ...supportedLocales === void 0 ? {} : { supportedLocales },
318
358
  ...translations === void 0 ? {} : { translations },
319
359
  fields: sanitizedFields
320
360
  };
@@ -367,6 +407,12 @@ function isRecord(value) {
367
407
  function isNonEmptyString(value) {
368
408
  return typeof value === "string" && value.trim().length > 0;
369
409
  }
410
+ function localeRecordEntry(record, locale) {
411
+ for (const [candidate, value] of Object.entries(record ?? {})) {
412
+ if (canonicalLocaleOrRaw(candidate) === locale) return value;
413
+ }
414
+ return void 0;
415
+ }
370
416
  function issue(issues, path, code, message) {
371
417
  issues.push({ path, code, message });
372
418
  }
@@ -729,51 +775,55 @@ function collectSchemaText(schema) {
729
775
  return entries;
730
776
  }
731
777
  function addRequiredTranslationIssues(schema, locale, issues) {
732
- if (!(schema.supportedLocales ?? []).includes(locale)) {
778
+ if (!(schema.supportedLocales ?? []).some((candidate) => canonicalLocaleOrRaw(candidate) === locale)) {
733
779
  issue(issues, "supportedLocales", "required_locale_missing", `Required locale ${locale} is missing.`);
734
780
  }
735
- if (locale === schema.defaultLocale) return;
781
+ if (schema.defaultLocale !== void 0 && canonicalLocaleOrRaw(schema.defaultLocale) === locale) return;
782
+ const formTranslation = localeRecordEntry(schema.translations, locale);
736
783
  const required = [
737
- { path: `translations.${locale}.title`, value: schema.translations?.[locale]?.title }
784
+ { path: `translations.${locale}.title`, value: formTranslation?.title }
738
785
  ];
739
786
  if (schema.description !== void 0)
740
- required.push({ path: `translations.${locale}.description`, value: schema.translations?.[locale]?.description });
787
+ required.push({ path: `translations.${locale}.description`, value: formTranslation?.description });
741
788
  if (schema.completionMessage !== void 0) {
742
789
  required.push({
743
790
  path: `translations.${locale}.completionMessage`,
744
- value: schema.translations?.[locale]?.completionMessage
791
+ value: formTranslation?.completionMessage
745
792
  });
746
793
  }
747
794
  schema.fields.forEach((field, fieldIndex) => {
795
+ const fieldTranslation = localeRecordEntry(field.translations, locale);
748
796
  required.push({
749
797
  path: `fields[${fieldIndex}].translations.${locale}.title`,
750
- value: field.translations?.[locale]?.title
798
+ value: fieldTranslation?.title
751
799
  });
752
800
  if (field.description !== void 0) {
753
801
  required.push({
754
802
  path: `fields[${fieldIndex}].translations.${locale}.description`,
755
- value: field.translations?.[locale]?.description
803
+ value: fieldTranslation?.description
756
804
  });
757
805
  }
758
806
  if (!("options" in field)) return;
759
807
  field.options.forEach((option, optionIndex) => {
808
+ const optionTranslation = localeRecordEntry(option.translations, locale);
760
809
  required.push({
761
810
  path: `fields[${fieldIndex}].options[${optionIndex}].translations.${locale}`,
762
- value: option.translations?.[locale]
811
+ value: optionTranslation
763
812
  });
764
813
  });
765
814
  });
766
815
  schema.pages?.forEach((page, pageIndex) => {
816
+ const pageTranslation = localeRecordEntry(page.translations, locale);
767
817
  if (page.title !== void 0) {
768
818
  required.push({
769
819
  path: `pages[${pageIndex}].translations.${locale}.title`,
770
- value: page.translations?.[locale]?.title
820
+ value: pageTranslation?.title
771
821
  });
772
822
  }
773
823
  if (page.description !== void 0) {
774
824
  required.push({
775
825
  path: `pages[${pageIndex}].translations.${locale}.description`,
776
- value: page.translations?.[locale]?.description
826
+ value: pageTranslation?.description
777
827
  });
778
828
  }
779
829
  });
@@ -906,8 +956,8 @@ function validatePolicy(schema, policy, issues) {
906
956
  }
907
957
  const collectedLocales = collectSchemaLocales(schema);
908
958
  const registeredLocales = /* @__PURE__ */ new Set([
909
- ...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
910
- ...schema.supportedLocales ?? []
959
+ ...collectedLocales.defaultLocale === void 0 ? [] : [collectedLocales.defaultLocale],
960
+ ...collectedLocales.supportedLocales
911
961
  ]);
912
962
  for (const locale of collectedLocales.translationLocales) {
913
963
  if (registeredLocales.has(locale)) continue;
@@ -921,23 +971,27 @@ function validatePolicy(schema, policy, issues) {
921
971
  }
922
972
  }
923
973
  if (policy.allowedLocales !== void 0) {
974
+ const allowedLocales = new Set(policy.allowedLocales.map(canonicalLocaleOrRaw));
924
975
  const pathsByLocale = /* @__PURE__ */ new Map();
925
- if (schema.defaultLocale !== void 0) pathsByLocale.set(schema.defaultLocale, ["defaultLocale"]);
976
+ if (collectedLocales.defaultLocale !== void 0)
977
+ pathsByLocale.set(collectedLocales.defaultLocale, ["defaultLocale"]);
926
978
  schema.supportedLocales?.forEach((locale, index) => {
927
- pathsByLocale.set(locale, [...pathsByLocale.get(locale) ?? [], `supportedLocales[${index}]`]);
979
+ const canonical = canonicalLocaleOrRaw(locale);
980
+ pathsByLocale.set(canonical, [...pathsByLocale.get(canonical) ?? [], `supportedLocales[${index}]`]);
928
981
  });
929
982
  for (const [locale, paths] of collectedLocales.translationLocalePaths) {
930
983
  pathsByLocale.set(locale, [...pathsByLocale.get(locale) ?? [], ...paths]);
931
984
  }
932
985
  for (const [locale, paths] of pathsByLocale) {
933
- if (!policy.allowedLocales.includes(locale)) {
986
+ if (!allowedLocales.has(locale)) {
934
987
  for (const path of paths) {
935
988
  issue(issues, path, "disallowed_locale", `Locale ${locale} is not allowed by the form policy.`);
936
989
  }
937
990
  }
938
991
  }
939
- for (const locale of policy.requiredLocales ?? []) {
940
- if (!policy.allowedLocales.includes(locale)) {
992
+ for (const rawLocale of policy.requiredLocales ?? []) {
993
+ const locale = canonicalLocaleOrRaw(rawLocale);
994
+ if (!allowedLocales.has(locale)) {
941
995
  issue(
942
996
  issues,
943
997
  "policy.requiredLocales",
@@ -950,7 +1004,8 @@ function validatePolicy(schema, policy, issues) {
950
1004
  if (policy.maxLocales !== void 0 && collectedLocales.allUniqueLocales.size > policy.maxLocales) {
951
1005
  issue(issues, "supportedLocales", "max_locales_exceeded", `At most ${policy.maxLocales} locales are allowed.`);
952
1006
  }
953
- for (const locale of policy.requiredLocales ?? []) addRequiredTranslationIssues(schema, locale, issues);
1007
+ for (const locale of policy.requiredLocales ?? [])
1008
+ addRequiredTranslationIssues(schema, canonicalLocaleOrRaw(locale), issues);
954
1009
  if (policy.maxSchemaBytes !== void 0) {
955
1010
  try {
956
1011
  const byteLength = new TextEncoder().encode(JSON.stringify(schema)).byteLength;
@@ -985,8 +1040,12 @@ function validateFormSchema(input, options = {}) {
985
1040
  );
986
1041
  }
987
1042
  }
988
- if (input.defaultLocale !== void 0 && !isNonEmptyString(input.defaultLocale)) {
989
- issue(issues, "defaultLocale", "invalid_locale", "Expected a non-empty default locale.");
1043
+ if (input.defaultLocale !== void 0) {
1044
+ if (!isNonEmptyString(input.defaultLocale)) {
1045
+ issue(issues, "defaultLocale", "invalid_locale", "Expected a non-empty default locale.");
1046
+ } else if (normalizeLocale(input.defaultLocale) === null) {
1047
+ issue(issues, "defaultLocale", "invalid_locale", "Expected a valid BCP 47 locale.");
1048
+ }
990
1049
  }
991
1050
  if (input.supportedLocales !== void 0) {
992
1051
  if (!Array.isArray(input.supportedLocales) || input.supportedLocales.length === 0) {
@@ -996,10 +1055,15 @@ function validateFormSchema(input, options = {}) {
996
1055
  input.supportedLocales.forEach((locale, index) => {
997
1056
  if (!isNonEmptyString(locale)) {
998
1057
  issue(issues, `supportedLocales[${index}]`, "invalid_locale", "Expected a non-empty locale.");
999
- } else if (locales.has(locale)) {
1000
- issue(issues, `supportedLocales[${index}]`, "duplicate_locale", "Locales must be unique.");
1001
1058
  } else {
1002
- locales.add(locale);
1059
+ const normalized = normalizeLocale(locale);
1060
+ if (normalized === null) {
1061
+ issue(issues, `supportedLocales[${index}]`, "invalid_locale", "Expected a valid BCP 47 locale.");
1062
+ } else if (locales.has(normalized)) {
1063
+ issue(issues, `supportedLocales[${index}]`, "duplicate_locale", "Locales must be unique.");
1064
+ } else {
1065
+ locales.add(normalized);
1066
+ }
1003
1067
  }
1004
1068
  });
1005
1069
  }
@@ -2268,8 +2332,15 @@ function withTranslationMetadata(node, locale, property, metadata) {
2268
2332
  }
2269
2333
  };
2270
2334
  }
2271
- function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata, options = {}) {
2272
- const path = `${kind}.${nodeId}.${property}`;
2335
+ function localeRecordEntry2(record, locale) {
2336
+ const normalizedLocale = normalizeLocale(locale) ?? locale;
2337
+ for (const [candidate, value] of Object.entries(record ?? {})) {
2338
+ if ((normalizeLocale(candidate) ?? candidate) === normalizedLocale) return value;
2339
+ }
2340
+ return void 0;
2341
+ }
2342
+ function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata, options = {}, parentId) {
2343
+ const path = kind === "form" ? `form.${property}` : kind === "option" ? `fields.${parentId ?? ""}.options.${nodeId}.${property}` : `${kind}s.${nodeId}.${property}`;
2273
2344
  const manual = options.isManualTranslation?.(existingTranslationMetadata, { path, locale }) ?? isManualTranslationMetadata(existingTranslationMetadata);
2274
2345
  const normalizedMetadata = existingTranslationMetadata === void 0 ? void 0 : options.normalizeMetadata === void 0 ? existingTranslationMetadata : { ...options.normalizeMetadata(existingTranslationMetadata, sourceText) };
2275
2346
  const status = getTranslationStatusWithManualOverride(sourceText, existingText, normalizedMetadata, manual);
@@ -2289,7 +2360,10 @@ function createSlot(kind, nodeId, property, locale, sourceText, existingText, no
2289
2360
  };
2290
2361
  }
2291
2362
  function translationSlots(schema, locale, options = {}) {
2363
+ locale = normalizeLocale(locale) ?? locale;
2292
2364
  const descriptors = [];
2365
+ const schemaTranslation = localeRecordEntry2(schema.translations, locale);
2366
+ const schemaTranslationMetadata = localeRecordEntry2(schema.translationMetadata, locale);
2293
2367
  const addFormSlot = (property, sourceText) => {
2294
2368
  const slot = createSlot(
2295
2369
  "form",
@@ -2297,17 +2371,17 @@ function translationSlots(schema, locale, options = {}) {
2297
2371
  property,
2298
2372
  locale,
2299
2373
  sourceText,
2300
- schema.translations?.[locale]?.[property],
2374
+ schemaTranslation?.[property],
2301
2375
  schema.metadata,
2302
- schema.translationMetadata?.[locale]?.[property],
2376
+ schemaTranslationMetadata?.[property],
2303
2377
  options
2304
2378
  );
2305
2379
  descriptors.push({
2306
2380
  slot,
2307
- manual: options.isManualTranslation?.(schema.translationMetadata?.[locale]?.[property], {
2381
+ manual: options.isManualTranslation?.(schemaTranslationMetadata?.[property], {
2308
2382
  path: slot.path ?? "",
2309
2383
  locale
2310
- }) ?? isManualTranslationMetadata(schema.translationMetadata?.[locale]?.[property]),
2384
+ }) ?? isManualTranslationMetadata(schemaTranslationMetadata?.[property]),
2311
2385
  apply: (current, value, metadata) => withTranslationMetadata(
2312
2386
  { ...current, translations: mergeLocalizedText(current.translations, locale, property, value) },
2313
2387
  locale,
@@ -2320,6 +2394,8 @@ function translationSlots(schema, locale, options = {}) {
2320
2394
  if (schema.description !== void 0) addFormSlot("description", schema.description);
2321
2395
  if (schema.completionMessage !== void 0) addFormSlot("completionMessage", schema.completionMessage);
2322
2396
  schema.fields.forEach((field, fieldIndex) => {
2397
+ const fieldTranslation = localeRecordEntry2(field.translations, locale);
2398
+ const fieldTranslationMetadata = localeRecordEntry2(field.translationMetadata, locale);
2323
2399
  const addFieldSlot = (property, sourceText) => {
2324
2400
  const slot = createSlot(
2325
2401
  "field",
@@ -2327,17 +2403,17 @@ function translationSlots(schema, locale, options = {}) {
2327
2403
  property,
2328
2404
  locale,
2329
2405
  sourceText,
2330
- field.translations?.[locale]?.[property],
2406
+ fieldTranslation?.[property],
2331
2407
  field.metadata,
2332
- field.translationMetadata?.[locale]?.[property],
2408
+ fieldTranslationMetadata?.[property],
2333
2409
  options
2334
2410
  );
2335
2411
  descriptors.push({
2336
2412
  slot,
2337
- manual: options.isManualTranslation?.(field.translationMetadata?.[locale]?.[property], {
2413
+ manual: options.isManualTranslation?.(fieldTranslationMetadata?.[property], {
2338
2414
  path: slot.path ?? "",
2339
2415
  locale
2340
- }) ?? isManualTranslationMetadata(field.translationMetadata?.[locale]?.[property]),
2416
+ }) ?? isManualTranslationMetadata(fieldTranslationMetadata?.[property]),
2341
2417
  apply: (current, value, metadata) => ({
2342
2418
  ...current,
2343
2419
  fields: current.fields.map(
@@ -2358,23 +2434,26 @@ function translationSlots(schema, locale, options = {}) {
2358
2434
  if (field.description !== void 0) addFieldSlot("description", field.description);
2359
2435
  if ("options" in field) {
2360
2436
  field.options.forEach((option, optionIndex) => {
2437
+ const optionTranslation = localeRecordEntry2(option.translations, locale);
2438
+ const optionTranslationMetadata = localeRecordEntry2(option.translationMetadata, locale);
2361
2439
  const slot = createSlot(
2362
2440
  "option",
2363
2441
  option.id,
2364
2442
  "label",
2365
2443
  locale,
2366
2444
  option.label,
2367
- option.translations?.[locale],
2445
+ optionTranslation,
2368
2446
  option.metadata,
2369
- option.translationMetadata?.[locale]?.label,
2370
- options
2447
+ optionTranslationMetadata?.label,
2448
+ options,
2449
+ field.id
2371
2450
  );
2372
2451
  descriptors.push({
2373
2452
  slot,
2374
- manual: options.isManualTranslation?.(option.translationMetadata?.[locale]?.label, {
2453
+ manual: options.isManualTranslation?.(optionTranslationMetadata?.label, {
2375
2454
  path: slot.path ?? "",
2376
2455
  locale
2377
- }) ?? isManualTranslationMetadata(option.translationMetadata?.[locale]?.label),
2456
+ }) ?? isManualTranslationMetadata(optionTranslationMetadata?.label),
2378
2457
  apply: (current, value, metadata) => ({
2379
2458
  ...current,
2380
2459
  fields: current.fields.map((candidate, candidateIndex) => {
@@ -2400,6 +2479,8 @@ function translationSlots(schema, locale, options = {}) {
2400
2479
  }
2401
2480
  });
2402
2481
  schema.pages?.forEach((page, pageIndex) => {
2482
+ const pageTranslation = localeRecordEntry2(page.translations, locale);
2483
+ const pageTranslationMetadata = localeRecordEntry2(page.translationMetadata, locale);
2403
2484
  const addPageSlot = (property, sourceText) => {
2404
2485
  const slot = createSlot(
2405
2486
  "page",
@@ -2407,17 +2488,17 @@ function translationSlots(schema, locale, options = {}) {
2407
2488
  property,
2408
2489
  locale,
2409
2490
  sourceText,
2410
- page.translations?.[locale]?.[property],
2491
+ pageTranslation?.[property],
2411
2492
  page.metadata,
2412
- page.translationMetadata?.[locale]?.[property],
2493
+ pageTranslationMetadata?.[property],
2413
2494
  options
2414
2495
  );
2415
2496
  descriptors.push({
2416
2497
  slot,
2417
- manual: options.isManualTranslation?.(page.translationMetadata?.[locale]?.[property], {
2498
+ manual: options.isManualTranslation?.(pageTranslationMetadata?.[property], {
2418
2499
  path: slot.path ?? "",
2419
2500
  locale
2420
- }) ?? isManualTranslationMetadata(page.translationMetadata?.[locale]?.[property]),
2501
+ }) ?? isManualTranslationMetadata(pageTranslationMetadata?.[property]),
2421
2502
  apply: (current, value, metadata) => ({
2422
2503
  ...current,
2423
2504
  ...current.pages === void 0 ? {} : {
@@ -2456,10 +2537,9 @@ function removeLocalizedNodeLocale(node, locale) {
2456
2537
  ...translationMetadata === void 0 ? {} : { translationMetadata }
2457
2538
  };
2458
2539
  }
2459
- function migrateMetadata(metadata, sourceText, defaultLocale, customMigrator) {
2460
- if (customMigrator !== void 0) return customMigrator(metadata, sourceText);
2540
+ function defaultTranslationMetadataMigrator(metadata, sourceText, defaultLocale) {
2461
2541
  const record = metadata !== null && typeof metadata === "object" ? metadata : void 0;
2462
- const sourceLocale = typeof record?.sourceLocale === "string" ? record.sourceLocale : defaultLocale ?? "";
2542
+ const sourceLocale = typeof record?.sourceLocale === "string" ? record.sourceLocale : defaultLocale;
2463
2543
  const translationSource = isManualTranslationMetadata(record) ? "manual" : "automatic";
2464
2544
  return {
2465
2545
  sourceLocale,
@@ -2469,22 +2549,33 @@ function migrateMetadata(metadata, sourceText, defaultLocale, customMigrator) {
2469
2549
  ...typeof record?.editedAt === "string" ? { editedAt: record.editedAt } : {}
2470
2550
  };
2471
2551
  }
2472
- function migrateNodeMetadata(node, sourceTexts, defaultLocale, customMigrator) {
2552
+ function migrateMetadata(metadata, sourceText, context, migrator) {
2553
+ return migrator(metadata, sourceText, context);
2554
+ }
2555
+ function isTranslationMetadataProperty(property) {
2556
+ return property === "title" || property === "description" || property === "label" || property === "completionMessage";
2557
+ }
2558
+ function migrateNodeMetadata(node, sourceTexts, contextFor, migrator) {
2473
2559
  if (node.translationMetadata === void 0) return node;
2474
2560
  const translationMetadata = Object.fromEntries(
2475
2561
  Object.entries(node.translationMetadata).map(([locale, properties]) => [
2476
2562
  locale,
2477
2563
  Object.fromEntries(
2478
- Object.entries(properties).map(([property, metadata]) => [
2479
- property,
2480
- migrateMetadata(metadata, sourceTexts[property] ?? "", defaultLocale, customMigrator)
2481
- ])
2564
+ Object.entries(properties).map(([property, metadata]) => {
2565
+ if (!isTranslationMetadataProperty(property)) return [property, metadata];
2566
+ return [
2567
+ property,
2568
+ migrateMetadata(metadata, sourceTexts[property] ?? "", contextFor(locale, property), migrator)
2569
+ ];
2570
+ })
2482
2571
  )
2483
2572
  ])
2484
2573
  );
2485
2574
  return { ...node, translationMetadata };
2486
2575
  }
2487
- var migrateSchemaTranslationMetadata = (schema, customMigrator) => {
2576
+ var migrateSchemaTranslationMetadata = (schema, migratorOrOptions) => {
2577
+ const defaultLocale = schema.defaultLocale ?? "";
2578
+ const migrator = typeof migratorOrOptions === "function" ? migratorOrOptions : migratorOrOptions?.migrator ?? ((metadata, sourceText, context) => defaultTranslationMetadataMigrator(metadata, sourceText, context.defaultLocale));
2488
2579
  const migratedSchema = migrateNodeMetadata(
2489
2580
  schema,
2490
2581
  {
@@ -2492,21 +2583,47 @@ var migrateSchemaTranslationMetadata = (schema, customMigrator) => {
2492
2583
  ...schema.description === void 0 ? {} : { description: schema.description },
2493
2584
  ...schema.completionMessage === void 0 ? {} : { completionMessage: schema.completionMessage }
2494
2585
  },
2495
- schema.defaultLocale,
2496
- customMigrator
2586
+ (locale, property) => ({
2587
+ locale,
2588
+ defaultLocale,
2589
+ path: `form.${property}`,
2590
+ property,
2591
+ nodeKind: "form"
2592
+ }),
2593
+ migrator
2497
2594
  );
2498
2595
  const fields = schema.fields.map((field) => {
2499
2596
  const migratedField = migrateNodeMetadata(
2500
2597
  field,
2501
2598
  { title: field.title, ...field.description === void 0 ? {} : { description: field.description } },
2502
- schema.defaultLocale,
2503
- customMigrator
2599
+ (locale, property) => ({
2600
+ locale,
2601
+ defaultLocale,
2602
+ path: `fields.${field.id}.${property}`,
2603
+ property,
2604
+ nodeKind: "field",
2605
+ nodeId: field.id
2606
+ }),
2607
+ migrator
2504
2608
  );
2505
2609
  if (!("options" in migratedField)) return migratedField;
2506
2610
  return {
2507
2611
  ...migratedField,
2508
2612
  options: migratedField.options.map(
2509
- (option) => migrateNodeMetadata(option, { label: option.label }, schema.defaultLocale, customMigrator)
2613
+ (option) => migrateNodeMetadata(
2614
+ option,
2615
+ { label: option.label },
2616
+ (locale, property) => ({
2617
+ locale,
2618
+ defaultLocale,
2619
+ path: `fields.${field.id}.options.${option.id}.${property}`,
2620
+ property,
2621
+ nodeKind: "option",
2622
+ nodeId: option.id,
2623
+ parentId: field.id
2624
+ }),
2625
+ migrator
2626
+ )
2510
2627
  )
2511
2628
  };
2512
2629
  });
@@ -2517,8 +2634,15 @@ var migrateSchemaTranslationMetadata = (schema, customMigrator) => {
2517
2634
  ...page.title === void 0 ? {} : { title: page.title },
2518
2635
  ...page.description === void 0 ? {} : { description: page.description }
2519
2636
  },
2520
- schema.defaultLocale,
2521
- customMigrator
2637
+ (locale, property) => ({
2638
+ locale,
2639
+ defaultLocale,
2640
+ path: `pages.${page.id}.${property}`,
2641
+ property,
2642
+ nodeKind: "page",
2643
+ nodeId: page.id
2644
+ }),
2645
+ migrator
2522
2646
  )
2523
2647
  );
2524
2648
  return {
@@ -2561,8 +2685,11 @@ function collectTranslationSlots(schema, locale) {
2561
2685
  return translationSlots(schema, locale).map((descriptor) => descriptor.slot);
2562
2686
  }
2563
2687
  function resolveLocalizedSchema(schema, targetLocale) {
2564
- if (targetLocale === void 0 || targetLocale.length === 0 || targetLocale === schema.defaultLocale) return schema;
2565
- const formTranslation = schema.translations?.[targetLocale];
2688
+ if (targetLocale === void 0 || targetLocale.length === 0) return schema;
2689
+ const normalizedTargetLocale = normalizeLocale(targetLocale) ?? targetLocale;
2690
+ const defaultLocale = schema.defaultLocale === void 0 ? void 0 : normalizeLocale(schema.defaultLocale);
2691
+ if (normalizedTargetLocale === defaultLocale || targetLocale === schema.defaultLocale) return schema;
2692
+ const formTranslation = localeRecordEntry2(schema.translations, normalizedTargetLocale);
2566
2693
  const completionMessage = formTranslation?.completionMessage ?? schema.completionMessage;
2567
2694
  return {
2568
2695
  ...schema,
@@ -2570,7 +2697,7 @@ function resolveLocalizedSchema(schema, targetLocale) {
2570
2697
  ...(formTranslation?.description ?? schema.description) === void 0 ? {} : { description: formTranslation?.description ?? schema.description },
2571
2698
  ...completionMessage === void 0 ? {} : { completionMessage },
2572
2699
  fields: schema.fields.map((field) => {
2573
- const translation = field.translations?.[targetLocale];
2700
+ const translation = localeRecordEntry2(field.translations, normalizedTargetLocale);
2574
2701
  const localized = {
2575
2702
  ...field,
2576
2703
  title: translation?.title ?? field.title,
@@ -2581,13 +2708,13 @@ function resolveLocalizedSchema(schema, targetLocale) {
2581
2708
  ...localized,
2582
2709
  options: field.options.map((option) => ({
2583
2710
  ...option,
2584
- label: option.translations?.[targetLocale] ?? option.label
2711
+ label: localeRecordEntry2(option.translations, normalizedTargetLocale) ?? option.label
2585
2712
  }))
2586
2713
  };
2587
2714
  }),
2588
2715
  ...schema.pages === void 0 ? {} : {
2589
2716
  pages: schema.pages.map((page) => {
2590
- const translation = page.translations?.[targetLocale];
2717
+ const translation = localeRecordEntry2(page.translations, normalizedTargetLocale);
2591
2718
  const title = translation?.title ?? page.title;
2592
2719
  const description = translation?.description ?? page.description;
2593
2720
  return {
@@ -2601,8 +2728,13 @@ function resolveLocalizedSchema(schema, targetLocale) {
2601
2728
  }
2602
2729
  async function populateSchemaTranslations(schema, targetLocales, adapter, options = {}) {
2603
2730
  assertValidFormSchema(schema);
2604
- const locales = [...new Set(targetLocales.filter((locale) => locale.length > 0 && locale !== schema.defaultLocale))];
2605
- const allowedLocales = options.policy?.allowedLocales;
2731
+ const defaultLocale = schema.defaultLocale === void 0 ? void 0 : normalizeLocale(schema.defaultLocale) ?? schema.defaultLocale;
2732
+ const locales = [
2733
+ ...new Set(
2734
+ targetLocales.map((locale) => normalizeLocale(locale) ?? locale.trim()).filter((locale) => locale.length > 0 && locale !== defaultLocale)
2735
+ )
2736
+ ];
2737
+ const allowedLocales = options.policy?.allowedLocales?.map((locale) => normalizeLocale(locale) ?? locale);
2606
2738
  const collectedLocales = collectSchemaLocales(schema);
2607
2739
  const disallowedLocale = [...collectedLocales.allUniqueLocales, ...locales].find(
2608
2740
  (locale) => allowedLocales !== void 0 && !allowedLocales.includes(locale)
@@ -2644,7 +2776,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2644
2776
  adapter,
2645
2777
  selected.map((descriptor) => descriptor.slot.sourceText),
2646
2778
  locale,
2647
- schema.defaultLocale
2779
+ defaultLocale
2648
2780
  );
2649
2781
  if (translated.length !== selected.length) {
2650
2782
  throw new Error(`Translation adapter returned ${translated.length} texts for ${selected.length} inputs.`);
@@ -2653,7 +2785,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2653
2785
  const translatedText = translated[index];
2654
2786
  if (translatedText === void 0) throw new Error("Translation adapter returned an unexpected result.");
2655
2787
  const metadata = options.createMetadata?.(descriptor.slot, translatedText) ?? {
2656
- sourceLocale: schema.defaultLocale ?? "",
2788
+ sourceLocale: defaultLocale ?? "",
2657
2789
  sourceTextHash: computeSourceTextHash(descriptor.slot.sourceText),
2658
2790
  translationSource: "automatic",
2659
2791
  translatedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -2664,8 +2796,8 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2664
2796
  }
2665
2797
  const supportedLocales = [
2666
2798
  .../* @__PURE__ */ new Set([
2667
- ...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
2668
- ...schema.supportedLocales ?? [],
2799
+ ...defaultLocale === void 0 ? [] : [defaultLocale],
2800
+ ...(schema.supportedLocales ?? []).map((locale) => normalizeLocale(locale) ?? locale),
2669
2801
  ...locales
2670
2802
  ])
2671
2803
  ];
@@ -3001,6 +3133,7 @@ function assertVersionMutable(status) {
3001
3133
  matchesSubmissionFilter,
3002
3134
  matchesSubmissionPageFilters,
3003
3135
  migrateSchemaTranslationMetadata,
3136
+ normalizeLocale,
3004
3137
  normalizeSubmissionPageSize,
3005
3138
  pipeResponsesToCsvStream,
3006
3139
  populateSchemaTranslations,