@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/dist/index.js CHANGED
@@ -1,8 +1,24 @@
1
+ // src/locale.ts
2
+ var normalizeLocale = (rawLocale) => {
3
+ if (!rawLocale || typeof rawLocale !== "string") return null;
4
+ const trimmed = rawLocale.trim().replace(/_/gu, "-");
5
+ if (trimmed.length === 0) return null;
6
+ try {
7
+ return Intl.getCanonicalLocales(trimmed)[0] ?? null;
8
+ } catch {
9
+ return null;
10
+ }
11
+ };
12
+ function canonicalLocaleOrRaw(rawLocale) {
13
+ return normalizeLocale(rawLocale) ?? rawLocale;
14
+ }
15
+
1
16
  // src/policy.ts
2
17
  function collectRecordKeys(value, path, pathsByLocale) {
3
- for (const locale of Object.keys(value ?? {})) {
18
+ for (const rawLocale of Object.keys(value ?? {})) {
19
+ const locale = canonicalLocaleOrRaw(rawLocale);
4
20
  const paths = pathsByLocale.get(locale) ?? [];
5
- paths.push(`${path}.${locale}`);
21
+ paths.push(`${path}.${rawLocale}`);
6
22
  pathsByLocale.set(locale, paths);
7
23
  }
8
24
  }
@@ -33,13 +49,15 @@ function collectSchemaLocales(schema) {
33
49
  });
34
50
  const translationLocales = new Set(pathsByLocale.keys());
35
51
  const allUniqueLocales = /* @__PURE__ */ new Set([
36
- ...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
37
- ...schema.supportedLocales ?? [],
52
+ ...schema.defaultLocale === void 0 ? [] : [canonicalLocaleOrRaw(schema.defaultLocale)],
53
+ ...(schema.supportedLocales ?? []).map(canonicalLocaleOrRaw),
38
54
  ...translationLocales
39
55
  ]);
56
+ const defaultLocale = schema.defaultLocale === void 0 ? void 0 : canonicalLocaleOrRaw(schema.defaultLocale);
57
+ const supportedLocales = (schema.supportedLocales ?? []).map(canonicalLocaleOrRaw);
40
58
  return {
41
- ...schema.defaultLocale === void 0 ? {} : { defaultLocale: schema.defaultLocale },
42
- supportedLocales: schema.supportedLocales ?? [],
59
+ ...defaultLocale === void 0 ? {} : { defaultLocale },
60
+ supportedLocales,
43
61
  translationLocales,
44
62
  allUniqueLocales,
45
63
  translationLocalePaths: pathsByLocale
@@ -62,7 +80,12 @@ function displayRuleSourceIds(field) {
62
80
  }
63
81
  function registeredEntries(value, registeredLocales) {
64
82
  if (value === void 0) return void 0;
65
- const entries = Object.entries(value).filter(([locale]) => registeredLocales.has(locale));
83
+ const entries = [];
84
+ for (const [rawLocale, entry] of Object.entries(value)) {
85
+ const locale = normalizeLocale(rawLocale);
86
+ if (locale === null || !registeredLocales.has(locale)) continue;
87
+ entries.push([locale, entry]);
88
+ }
66
89
  return entries.length === 0 ? void 0 : Object.fromEntries(entries);
67
90
  }
68
91
  function sanitizeNodeLocales(node, registeredLocales) {
@@ -100,6 +123,16 @@ function sanitizePageLocales(page, registeredLocales) {
100
123
  const translations = registeredEntries(page.translations, registeredLocales);
101
124
  return { ...base, ...translations === void 0 ? {} : { translations } };
102
125
  }
126
+ function normalizedLocaleList(locales) {
127
+ return [
128
+ ...new Set(
129
+ locales.flatMap((locale) => {
130
+ const normalized = normalizeLocale(locale);
131
+ return normalized === null ? [] : [normalized];
132
+ })
133
+ )
134
+ ];
135
+ }
103
136
  function sanitizeFieldConstraints(field, policy) {
104
137
  const constraint = policy?.fieldConstraints?.[field.type];
105
138
  if (constraint === void 0) return field;
@@ -214,10 +247,9 @@ function validateSchemaStructure(schema) {
214
247
  }
215
248
  function sanitizeSchema(schema, options = {}) {
216
249
  const existingQuestionIds = new Set(schema.fields.map((field) => field.id));
217
- const registeredLocales = /* @__PURE__ */ new Set([
218
- ...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
219
- ...schema.supportedLocales ?? []
220
- ]);
250
+ const defaultLocale = schema.defaultLocale === void 0 ? null : normalizeLocale(schema.defaultLocale);
251
+ const supportedLocales = schema.supportedLocales === void 0 ? void 0 : normalizedLocaleList(schema.supportedLocales);
252
+ const registeredLocales = /* @__PURE__ */ new Set([...defaultLocale === null ? [] : [defaultLocale], ...supportedLocales ?? []]);
221
253
  const cyclic = cyclicQuestionIds(schema.fields);
222
254
  const sanitizedFields = schema.fields.map((sourceField) => {
223
255
  const field = sanitizeFieldConstraints(sanitizeFieldLocales(sourceField, registeredLocales), options.policy);
@@ -235,10 +267,17 @@ function sanitizeSchema(schema, options = {}) {
235
267
  return sanitized;
236
268
  });
237
269
  const localizedSchema = sanitizeNodeLocales(schema, registeredLocales);
238
- const { translations: _translations, ...schemaWithoutLocaleContent } = localizedSchema;
270
+ const {
271
+ defaultLocale: _defaultLocale,
272
+ supportedLocales: _supportedLocales,
273
+ translations: _translations,
274
+ ...schemaWithoutLocaleContent
275
+ } = localizedSchema;
239
276
  const translations = registeredEntries(schema.translations, registeredLocales);
240
277
  const base = {
241
278
  ...schemaWithoutLocaleContent,
279
+ ...defaultLocale === null ? {} : { defaultLocale },
280
+ ...supportedLocales === void 0 ? {} : { supportedLocales },
242
281
  ...translations === void 0 ? {} : { translations },
243
282
  fields: sanitizedFields
244
283
  };
@@ -291,6 +330,12 @@ function isRecord(value) {
291
330
  function isNonEmptyString(value) {
292
331
  return typeof value === "string" && value.trim().length > 0;
293
332
  }
333
+ function localeRecordEntry(record, locale) {
334
+ for (const [candidate, value] of Object.entries(record ?? {})) {
335
+ if (canonicalLocaleOrRaw(candidate) === locale) return value;
336
+ }
337
+ return void 0;
338
+ }
294
339
  function issue(issues, path, code, message) {
295
340
  issues.push({ path, code, message });
296
341
  }
@@ -653,51 +698,55 @@ function collectSchemaText(schema) {
653
698
  return entries;
654
699
  }
655
700
  function addRequiredTranslationIssues(schema, locale, issues) {
656
- if (!(schema.supportedLocales ?? []).includes(locale)) {
701
+ if (!(schema.supportedLocales ?? []).some((candidate) => canonicalLocaleOrRaw(candidate) === locale)) {
657
702
  issue(issues, "supportedLocales", "required_locale_missing", `Required locale ${locale} is missing.`);
658
703
  }
659
- if (locale === schema.defaultLocale) return;
704
+ if (schema.defaultLocale !== void 0 && canonicalLocaleOrRaw(schema.defaultLocale) === locale) return;
705
+ const formTranslation = localeRecordEntry(schema.translations, locale);
660
706
  const required = [
661
- { path: `translations.${locale}.title`, value: schema.translations?.[locale]?.title }
707
+ { path: `translations.${locale}.title`, value: formTranslation?.title }
662
708
  ];
663
709
  if (schema.description !== void 0)
664
- required.push({ path: `translations.${locale}.description`, value: schema.translations?.[locale]?.description });
710
+ required.push({ path: `translations.${locale}.description`, value: formTranslation?.description });
665
711
  if (schema.completionMessage !== void 0) {
666
712
  required.push({
667
713
  path: `translations.${locale}.completionMessage`,
668
- value: schema.translations?.[locale]?.completionMessage
714
+ value: formTranslation?.completionMessage
669
715
  });
670
716
  }
671
717
  schema.fields.forEach((field, fieldIndex) => {
718
+ const fieldTranslation = localeRecordEntry(field.translations, locale);
672
719
  required.push({
673
720
  path: `fields[${fieldIndex}].translations.${locale}.title`,
674
- value: field.translations?.[locale]?.title
721
+ value: fieldTranslation?.title
675
722
  });
676
723
  if (field.description !== void 0) {
677
724
  required.push({
678
725
  path: `fields[${fieldIndex}].translations.${locale}.description`,
679
- value: field.translations?.[locale]?.description
726
+ value: fieldTranslation?.description
680
727
  });
681
728
  }
682
729
  if (!("options" in field)) return;
683
730
  field.options.forEach((option, optionIndex) => {
731
+ const optionTranslation = localeRecordEntry(option.translations, locale);
684
732
  required.push({
685
733
  path: `fields[${fieldIndex}].options[${optionIndex}].translations.${locale}`,
686
- value: option.translations?.[locale]
734
+ value: optionTranslation
687
735
  });
688
736
  });
689
737
  });
690
738
  schema.pages?.forEach((page, pageIndex) => {
739
+ const pageTranslation = localeRecordEntry(page.translations, locale);
691
740
  if (page.title !== void 0) {
692
741
  required.push({
693
742
  path: `pages[${pageIndex}].translations.${locale}.title`,
694
- value: page.translations?.[locale]?.title
743
+ value: pageTranslation?.title
695
744
  });
696
745
  }
697
746
  if (page.description !== void 0) {
698
747
  required.push({
699
748
  path: `pages[${pageIndex}].translations.${locale}.description`,
700
- value: page.translations?.[locale]?.description
749
+ value: pageTranslation?.description
701
750
  });
702
751
  }
703
752
  });
@@ -830,8 +879,8 @@ function validatePolicy(schema, policy, issues) {
830
879
  }
831
880
  const collectedLocales = collectSchemaLocales(schema);
832
881
  const registeredLocales = /* @__PURE__ */ new Set([
833
- ...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
834
- ...schema.supportedLocales ?? []
882
+ ...collectedLocales.defaultLocale === void 0 ? [] : [collectedLocales.defaultLocale],
883
+ ...collectedLocales.supportedLocales
835
884
  ]);
836
885
  for (const locale of collectedLocales.translationLocales) {
837
886
  if (registeredLocales.has(locale)) continue;
@@ -845,23 +894,27 @@ function validatePolicy(schema, policy, issues) {
845
894
  }
846
895
  }
847
896
  if (policy.allowedLocales !== void 0) {
897
+ const allowedLocales = new Set(policy.allowedLocales.map(canonicalLocaleOrRaw));
848
898
  const pathsByLocale = /* @__PURE__ */ new Map();
849
- if (schema.defaultLocale !== void 0) pathsByLocale.set(schema.defaultLocale, ["defaultLocale"]);
899
+ if (collectedLocales.defaultLocale !== void 0)
900
+ pathsByLocale.set(collectedLocales.defaultLocale, ["defaultLocale"]);
850
901
  schema.supportedLocales?.forEach((locale, index) => {
851
- pathsByLocale.set(locale, [...pathsByLocale.get(locale) ?? [], `supportedLocales[${index}]`]);
902
+ const canonical = canonicalLocaleOrRaw(locale);
903
+ pathsByLocale.set(canonical, [...pathsByLocale.get(canonical) ?? [], `supportedLocales[${index}]`]);
852
904
  });
853
905
  for (const [locale, paths] of collectedLocales.translationLocalePaths) {
854
906
  pathsByLocale.set(locale, [...pathsByLocale.get(locale) ?? [], ...paths]);
855
907
  }
856
908
  for (const [locale, paths] of pathsByLocale) {
857
- if (!policy.allowedLocales.includes(locale)) {
909
+ if (!allowedLocales.has(locale)) {
858
910
  for (const path of paths) {
859
911
  issue(issues, path, "disallowed_locale", `Locale ${locale} is not allowed by the form policy.`);
860
912
  }
861
913
  }
862
914
  }
863
- for (const locale of policy.requiredLocales ?? []) {
864
- if (!policy.allowedLocales.includes(locale)) {
915
+ for (const rawLocale of policy.requiredLocales ?? []) {
916
+ const locale = canonicalLocaleOrRaw(rawLocale);
917
+ if (!allowedLocales.has(locale)) {
865
918
  issue(
866
919
  issues,
867
920
  "policy.requiredLocales",
@@ -874,7 +927,8 @@ function validatePolicy(schema, policy, issues) {
874
927
  if (policy.maxLocales !== void 0 && collectedLocales.allUniqueLocales.size > policy.maxLocales) {
875
928
  issue(issues, "supportedLocales", "max_locales_exceeded", `At most ${policy.maxLocales} locales are allowed.`);
876
929
  }
877
- for (const locale of policy.requiredLocales ?? []) addRequiredTranslationIssues(schema, locale, issues);
930
+ for (const locale of policy.requiredLocales ?? [])
931
+ addRequiredTranslationIssues(schema, canonicalLocaleOrRaw(locale), issues);
878
932
  if (policy.maxSchemaBytes !== void 0) {
879
933
  try {
880
934
  const byteLength = new TextEncoder().encode(JSON.stringify(schema)).byteLength;
@@ -909,8 +963,12 @@ function validateFormSchema(input, options = {}) {
909
963
  );
910
964
  }
911
965
  }
912
- if (input.defaultLocale !== void 0 && !isNonEmptyString(input.defaultLocale)) {
913
- issue(issues, "defaultLocale", "invalid_locale", "Expected a non-empty default locale.");
966
+ if (input.defaultLocale !== void 0) {
967
+ if (!isNonEmptyString(input.defaultLocale)) {
968
+ issue(issues, "defaultLocale", "invalid_locale", "Expected a non-empty default locale.");
969
+ } else if (normalizeLocale(input.defaultLocale) === null) {
970
+ issue(issues, "defaultLocale", "invalid_locale", "Expected a valid BCP 47 locale.");
971
+ }
914
972
  }
915
973
  if (input.supportedLocales !== void 0) {
916
974
  if (!Array.isArray(input.supportedLocales) || input.supportedLocales.length === 0) {
@@ -920,10 +978,15 @@ function validateFormSchema(input, options = {}) {
920
978
  input.supportedLocales.forEach((locale, index) => {
921
979
  if (!isNonEmptyString(locale)) {
922
980
  issue(issues, `supportedLocales[${index}]`, "invalid_locale", "Expected a non-empty locale.");
923
- } else if (locales.has(locale)) {
924
- issue(issues, `supportedLocales[${index}]`, "duplicate_locale", "Locales must be unique.");
925
981
  } else {
926
- locales.add(locale);
982
+ const normalized = normalizeLocale(locale);
983
+ if (normalized === null) {
984
+ issue(issues, `supportedLocales[${index}]`, "invalid_locale", "Expected a valid BCP 47 locale.");
985
+ } else if (locales.has(normalized)) {
986
+ issue(issues, `supportedLocales[${index}]`, "duplicate_locale", "Locales must be unique.");
987
+ } else {
988
+ locales.add(normalized);
989
+ }
927
990
  }
928
991
  });
929
992
  }
@@ -2192,8 +2255,15 @@ function withTranslationMetadata(node, locale, property, metadata) {
2192
2255
  }
2193
2256
  };
2194
2257
  }
2195
- function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata, options = {}) {
2196
- const path = `${kind}.${nodeId}.${property}`;
2258
+ function localeRecordEntry2(record, locale) {
2259
+ const normalizedLocale = normalizeLocale(locale) ?? locale;
2260
+ for (const [candidate, value] of Object.entries(record ?? {})) {
2261
+ if ((normalizeLocale(candidate) ?? candidate) === normalizedLocale) return value;
2262
+ }
2263
+ return void 0;
2264
+ }
2265
+ function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata, options = {}, parentId) {
2266
+ const path = kind === "form" ? `form.${property}` : kind === "option" ? `fields.${parentId ?? ""}.options.${nodeId}.${property}` : `${kind}s.${nodeId}.${property}`;
2197
2267
  const manual = options.isManualTranslation?.(existingTranslationMetadata, { path, locale }) ?? isManualTranslationMetadata(existingTranslationMetadata);
2198
2268
  const normalizedMetadata = existingTranslationMetadata === void 0 ? void 0 : options.normalizeMetadata === void 0 ? existingTranslationMetadata : { ...options.normalizeMetadata(existingTranslationMetadata, sourceText) };
2199
2269
  const status = getTranslationStatusWithManualOverride(sourceText, existingText, normalizedMetadata, manual);
@@ -2213,7 +2283,10 @@ function createSlot(kind, nodeId, property, locale, sourceText, existingText, no
2213
2283
  };
2214
2284
  }
2215
2285
  function translationSlots(schema, locale, options = {}) {
2286
+ locale = normalizeLocale(locale) ?? locale;
2216
2287
  const descriptors = [];
2288
+ const schemaTranslation = localeRecordEntry2(schema.translations, locale);
2289
+ const schemaTranslationMetadata = localeRecordEntry2(schema.translationMetadata, locale);
2217
2290
  const addFormSlot = (property, sourceText) => {
2218
2291
  const slot = createSlot(
2219
2292
  "form",
@@ -2221,17 +2294,17 @@ function translationSlots(schema, locale, options = {}) {
2221
2294
  property,
2222
2295
  locale,
2223
2296
  sourceText,
2224
- schema.translations?.[locale]?.[property],
2297
+ schemaTranslation?.[property],
2225
2298
  schema.metadata,
2226
- schema.translationMetadata?.[locale]?.[property],
2299
+ schemaTranslationMetadata?.[property],
2227
2300
  options
2228
2301
  );
2229
2302
  descriptors.push({
2230
2303
  slot,
2231
- manual: options.isManualTranslation?.(schema.translationMetadata?.[locale]?.[property], {
2304
+ manual: options.isManualTranslation?.(schemaTranslationMetadata?.[property], {
2232
2305
  path: slot.path ?? "",
2233
2306
  locale
2234
- }) ?? isManualTranslationMetadata(schema.translationMetadata?.[locale]?.[property]),
2307
+ }) ?? isManualTranslationMetadata(schemaTranslationMetadata?.[property]),
2235
2308
  apply: (current, value, metadata) => withTranslationMetadata(
2236
2309
  { ...current, translations: mergeLocalizedText(current.translations, locale, property, value) },
2237
2310
  locale,
@@ -2244,6 +2317,8 @@ function translationSlots(schema, locale, options = {}) {
2244
2317
  if (schema.description !== void 0) addFormSlot("description", schema.description);
2245
2318
  if (schema.completionMessage !== void 0) addFormSlot("completionMessage", schema.completionMessage);
2246
2319
  schema.fields.forEach((field, fieldIndex) => {
2320
+ const fieldTranslation = localeRecordEntry2(field.translations, locale);
2321
+ const fieldTranslationMetadata = localeRecordEntry2(field.translationMetadata, locale);
2247
2322
  const addFieldSlot = (property, sourceText) => {
2248
2323
  const slot = createSlot(
2249
2324
  "field",
@@ -2251,17 +2326,17 @@ function translationSlots(schema, locale, options = {}) {
2251
2326
  property,
2252
2327
  locale,
2253
2328
  sourceText,
2254
- field.translations?.[locale]?.[property],
2329
+ fieldTranslation?.[property],
2255
2330
  field.metadata,
2256
- field.translationMetadata?.[locale]?.[property],
2331
+ fieldTranslationMetadata?.[property],
2257
2332
  options
2258
2333
  );
2259
2334
  descriptors.push({
2260
2335
  slot,
2261
- manual: options.isManualTranslation?.(field.translationMetadata?.[locale]?.[property], {
2336
+ manual: options.isManualTranslation?.(fieldTranslationMetadata?.[property], {
2262
2337
  path: slot.path ?? "",
2263
2338
  locale
2264
- }) ?? isManualTranslationMetadata(field.translationMetadata?.[locale]?.[property]),
2339
+ }) ?? isManualTranslationMetadata(fieldTranslationMetadata?.[property]),
2265
2340
  apply: (current, value, metadata) => ({
2266
2341
  ...current,
2267
2342
  fields: current.fields.map(
@@ -2282,23 +2357,26 @@ function translationSlots(schema, locale, options = {}) {
2282
2357
  if (field.description !== void 0) addFieldSlot("description", field.description);
2283
2358
  if ("options" in field) {
2284
2359
  field.options.forEach((option, optionIndex) => {
2360
+ const optionTranslation = localeRecordEntry2(option.translations, locale);
2361
+ const optionTranslationMetadata = localeRecordEntry2(option.translationMetadata, locale);
2285
2362
  const slot = createSlot(
2286
2363
  "option",
2287
2364
  option.id,
2288
2365
  "label",
2289
2366
  locale,
2290
2367
  option.label,
2291
- option.translations?.[locale],
2368
+ optionTranslation,
2292
2369
  option.metadata,
2293
- option.translationMetadata?.[locale]?.label,
2294
- options
2370
+ optionTranslationMetadata?.label,
2371
+ options,
2372
+ field.id
2295
2373
  );
2296
2374
  descriptors.push({
2297
2375
  slot,
2298
- manual: options.isManualTranslation?.(option.translationMetadata?.[locale]?.label, {
2376
+ manual: options.isManualTranslation?.(optionTranslationMetadata?.label, {
2299
2377
  path: slot.path ?? "",
2300
2378
  locale
2301
- }) ?? isManualTranslationMetadata(option.translationMetadata?.[locale]?.label),
2379
+ }) ?? isManualTranslationMetadata(optionTranslationMetadata?.label),
2302
2380
  apply: (current, value, metadata) => ({
2303
2381
  ...current,
2304
2382
  fields: current.fields.map((candidate, candidateIndex) => {
@@ -2324,6 +2402,8 @@ function translationSlots(schema, locale, options = {}) {
2324
2402
  }
2325
2403
  });
2326
2404
  schema.pages?.forEach((page, pageIndex) => {
2405
+ const pageTranslation = localeRecordEntry2(page.translations, locale);
2406
+ const pageTranslationMetadata = localeRecordEntry2(page.translationMetadata, locale);
2327
2407
  const addPageSlot = (property, sourceText) => {
2328
2408
  const slot = createSlot(
2329
2409
  "page",
@@ -2331,17 +2411,17 @@ function translationSlots(schema, locale, options = {}) {
2331
2411
  property,
2332
2412
  locale,
2333
2413
  sourceText,
2334
- page.translations?.[locale]?.[property],
2414
+ pageTranslation?.[property],
2335
2415
  page.metadata,
2336
- page.translationMetadata?.[locale]?.[property],
2416
+ pageTranslationMetadata?.[property],
2337
2417
  options
2338
2418
  );
2339
2419
  descriptors.push({
2340
2420
  slot,
2341
- manual: options.isManualTranslation?.(page.translationMetadata?.[locale]?.[property], {
2421
+ manual: options.isManualTranslation?.(pageTranslationMetadata?.[property], {
2342
2422
  path: slot.path ?? "",
2343
2423
  locale
2344
- }) ?? isManualTranslationMetadata(page.translationMetadata?.[locale]?.[property]),
2424
+ }) ?? isManualTranslationMetadata(pageTranslationMetadata?.[property]),
2345
2425
  apply: (current, value, metadata) => ({
2346
2426
  ...current,
2347
2427
  ...current.pages === void 0 ? {} : {
@@ -2380,10 +2460,9 @@ function removeLocalizedNodeLocale(node, locale) {
2380
2460
  ...translationMetadata === void 0 ? {} : { translationMetadata }
2381
2461
  };
2382
2462
  }
2383
- function migrateMetadata(metadata, sourceText, defaultLocale, customMigrator) {
2384
- if (customMigrator !== void 0) return customMigrator(metadata, sourceText);
2463
+ function defaultTranslationMetadataMigrator(metadata, sourceText, defaultLocale) {
2385
2464
  const record = metadata !== null && typeof metadata === "object" ? metadata : void 0;
2386
- const sourceLocale = typeof record?.sourceLocale === "string" ? record.sourceLocale : defaultLocale ?? "";
2465
+ const sourceLocale = typeof record?.sourceLocale === "string" ? record.sourceLocale : defaultLocale;
2387
2466
  const translationSource = isManualTranslationMetadata(record) ? "manual" : "automatic";
2388
2467
  return {
2389
2468
  sourceLocale,
@@ -2393,22 +2472,33 @@ function migrateMetadata(metadata, sourceText, defaultLocale, customMigrator) {
2393
2472
  ...typeof record?.editedAt === "string" ? { editedAt: record.editedAt } : {}
2394
2473
  };
2395
2474
  }
2396
- function migrateNodeMetadata(node, sourceTexts, defaultLocale, customMigrator) {
2475
+ function migrateMetadata(metadata, sourceText, context, migrator) {
2476
+ return migrator(metadata, sourceText, context);
2477
+ }
2478
+ function isTranslationMetadataProperty(property) {
2479
+ return property === "title" || property === "description" || property === "label" || property === "completionMessage";
2480
+ }
2481
+ function migrateNodeMetadata(node, sourceTexts, contextFor, migrator) {
2397
2482
  if (node.translationMetadata === void 0) return node;
2398
2483
  const translationMetadata = Object.fromEntries(
2399
2484
  Object.entries(node.translationMetadata).map(([locale, properties]) => [
2400
2485
  locale,
2401
2486
  Object.fromEntries(
2402
- Object.entries(properties).map(([property, metadata]) => [
2403
- property,
2404
- migrateMetadata(metadata, sourceTexts[property] ?? "", defaultLocale, customMigrator)
2405
- ])
2487
+ Object.entries(properties).map(([property, metadata]) => {
2488
+ if (!isTranslationMetadataProperty(property)) return [property, metadata];
2489
+ return [
2490
+ property,
2491
+ migrateMetadata(metadata, sourceTexts[property] ?? "", contextFor(locale, property), migrator)
2492
+ ];
2493
+ })
2406
2494
  )
2407
2495
  ])
2408
2496
  );
2409
2497
  return { ...node, translationMetadata };
2410
2498
  }
2411
- var migrateSchemaTranslationMetadata = (schema, customMigrator) => {
2499
+ var migrateSchemaTranslationMetadata = (schema, migratorOrOptions) => {
2500
+ const defaultLocale = schema.defaultLocale ?? "";
2501
+ const migrator = typeof migratorOrOptions === "function" ? migratorOrOptions : migratorOrOptions?.migrator ?? ((metadata, sourceText, context) => defaultTranslationMetadataMigrator(metadata, sourceText, context.defaultLocale));
2412
2502
  const migratedSchema = migrateNodeMetadata(
2413
2503
  schema,
2414
2504
  {
@@ -2416,21 +2506,47 @@ var migrateSchemaTranslationMetadata = (schema, customMigrator) => {
2416
2506
  ...schema.description === void 0 ? {} : { description: schema.description },
2417
2507
  ...schema.completionMessage === void 0 ? {} : { completionMessage: schema.completionMessage }
2418
2508
  },
2419
- schema.defaultLocale,
2420
- customMigrator
2509
+ (locale, property) => ({
2510
+ locale,
2511
+ defaultLocale,
2512
+ path: `form.${property}`,
2513
+ property,
2514
+ nodeKind: "form"
2515
+ }),
2516
+ migrator
2421
2517
  );
2422
2518
  const fields = schema.fields.map((field) => {
2423
2519
  const migratedField = migrateNodeMetadata(
2424
2520
  field,
2425
2521
  { title: field.title, ...field.description === void 0 ? {} : { description: field.description } },
2426
- schema.defaultLocale,
2427
- customMigrator
2522
+ (locale, property) => ({
2523
+ locale,
2524
+ defaultLocale,
2525
+ path: `fields.${field.id}.${property}`,
2526
+ property,
2527
+ nodeKind: "field",
2528
+ nodeId: field.id
2529
+ }),
2530
+ migrator
2428
2531
  );
2429
2532
  if (!("options" in migratedField)) return migratedField;
2430
2533
  return {
2431
2534
  ...migratedField,
2432
2535
  options: migratedField.options.map(
2433
- (option) => migrateNodeMetadata(option, { label: option.label }, schema.defaultLocale, customMigrator)
2536
+ (option) => migrateNodeMetadata(
2537
+ option,
2538
+ { label: option.label },
2539
+ (locale, property) => ({
2540
+ locale,
2541
+ defaultLocale,
2542
+ path: `fields.${field.id}.options.${option.id}.${property}`,
2543
+ property,
2544
+ nodeKind: "option",
2545
+ nodeId: option.id,
2546
+ parentId: field.id
2547
+ }),
2548
+ migrator
2549
+ )
2434
2550
  )
2435
2551
  };
2436
2552
  });
@@ -2441,8 +2557,15 @@ var migrateSchemaTranslationMetadata = (schema, customMigrator) => {
2441
2557
  ...page.title === void 0 ? {} : { title: page.title },
2442
2558
  ...page.description === void 0 ? {} : { description: page.description }
2443
2559
  },
2444
- schema.defaultLocale,
2445
- customMigrator
2560
+ (locale, property) => ({
2561
+ locale,
2562
+ defaultLocale,
2563
+ path: `pages.${page.id}.${property}`,
2564
+ property,
2565
+ nodeKind: "page",
2566
+ nodeId: page.id
2567
+ }),
2568
+ migrator
2446
2569
  )
2447
2570
  );
2448
2571
  return {
@@ -2485,8 +2608,11 @@ function collectTranslationSlots(schema, locale) {
2485
2608
  return translationSlots(schema, locale).map((descriptor) => descriptor.slot);
2486
2609
  }
2487
2610
  function resolveLocalizedSchema(schema, targetLocale) {
2488
- if (targetLocale === void 0 || targetLocale.length === 0 || targetLocale === schema.defaultLocale) return schema;
2489
- const formTranslation = schema.translations?.[targetLocale];
2611
+ if (targetLocale === void 0 || targetLocale.length === 0) return schema;
2612
+ const normalizedTargetLocale = normalizeLocale(targetLocale) ?? targetLocale;
2613
+ const defaultLocale = schema.defaultLocale === void 0 ? void 0 : normalizeLocale(schema.defaultLocale);
2614
+ if (normalizedTargetLocale === defaultLocale || targetLocale === schema.defaultLocale) return schema;
2615
+ const formTranslation = localeRecordEntry2(schema.translations, normalizedTargetLocale);
2490
2616
  const completionMessage = formTranslation?.completionMessage ?? schema.completionMessage;
2491
2617
  return {
2492
2618
  ...schema,
@@ -2494,7 +2620,7 @@ function resolveLocalizedSchema(schema, targetLocale) {
2494
2620
  ...(formTranslation?.description ?? schema.description) === void 0 ? {} : { description: formTranslation?.description ?? schema.description },
2495
2621
  ...completionMessage === void 0 ? {} : { completionMessage },
2496
2622
  fields: schema.fields.map((field) => {
2497
- const translation = field.translations?.[targetLocale];
2623
+ const translation = localeRecordEntry2(field.translations, normalizedTargetLocale);
2498
2624
  const localized = {
2499
2625
  ...field,
2500
2626
  title: translation?.title ?? field.title,
@@ -2505,13 +2631,13 @@ function resolveLocalizedSchema(schema, targetLocale) {
2505
2631
  ...localized,
2506
2632
  options: field.options.map((option) => ({
2507
2633
  ...option,
2508
- label: option.translations?.[targetLocale] ?? option.label
2634
+ label: localeRecordEntry2(option.translations, normalizedTargetLocale) ?? option.label
2509
2635
  }))
2510
2636
  };
2511
2637
  }),
2512
2638
  ...schema.pages === void 0 ? {} : {
2513
2639
  pages: schema.pages.map((page) => {
2514
- const translation = page.translations?.[targetLocale];
2640
+ const translation = localeRecordEntry2(page.translations, normalizedTargetLocale);
2515
2641
  const title = translation?.title ?? page.title;
2516
2642
  const description = translation?.description ?? page.description;
2517
2643
  return {
@@ -2525,8 +2651,13 @@ function resolveLocalizedSchema(schema, targetLocale) {
2525
2651
  }
2526
2652
  async function populateSchemaTranslations(schema, targetLocales, adapter, options = {}) {
2527
2653
  assertValidFormSchema(schema);
2528
- const locales = [...new Set(targetLocales.filter((locale) => locale.length > 0 && locale !== schema.defaultLocale))];
2529
- const allowedLocales = options.policy?.allowedLocales;
2654
+ const defaultLocale = schema.defaultLocale === void 0 ? void 0 : normalizeLocale(schema.defaultLocale) ?? schema.defaultLocale;
2655
+ const locales = [
2656
+ ...new Set(
2657
+ targetLocales.map((locale) => normalizeLocale(locale) ?? locale.trim()).filter((locale) => locale.length > 0 && locale !== defaultLocale)
2658
+ )
2659
+ ];
2660
+ const allowedLocales = options.policy?.allowedLocales?.map((locale) => normalizeLocale(locale) ?? locale);
2530
2661
  const collectedLocales = collectSchemaLocales(schema);
2531
2662
  const disallowedLocale = [...collectedLocales.allUniqueLocales, ...locales].find(
2532
2663
  (locale) => allowedLocales !== void 0 && !allowedLocales.includes(locale)
@@ -2568,7 +2699,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2568
2699
  adapter,
2569
2700
  selected.map((descriptor) => descriptor.slot.sourceText),
2570
2701
  locale,
2571
- schema.defaultLocale
2702
+ defaultLocale
2572
2703
  );
2573
2704
  if (translated.length !== selected.length) {
2574
2705
  throw new Error(`Translation adapter returned ${translated.length} texts for ${selected.length} inputs.`);
@@ -2577,7 +2708,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2577
2708
  const translatedText = translated[index];
2578
2709
  if (translatedText === void 0) throw new Error("Translation adapter returned an unexpected result.");
2579
2710
  const metadata = options.createMetadata?.(descriptor.slot, translatedText) ?? {
2580
- sourceLocale: schema.defaultLocale ?? "",
2711
+ sourceLocale: defaultLocale ?? "",
2581
2712
  sourceTextHash: computeSourceTextHash(descriptor.slot.sourceText),
2582
2713
  translationSource: "automatic",
2583
2714
  translatedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -2588,8 +2719,8 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2588
2719
  }
2589
2720
  const supportedLocales = [
2590
2721
  .../* @__PURE__ */ new Set([
2591
- ...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
2592
- ...schema.supportedLocales ?? [],
2722
+ ...defaultLocale === void 0 ? [] : [defaultLocale],
2723
+ ...(schema.supportedLocales ?? []).map((locale) => normalizeLocale(locale) ?? locale),
2593
2724
  ...locales
2594
2725
  ])
2595
2726
  ];
@@ -2924,6 +3055,7 @@ export {
2924
3055
  matchesSubmissionFilter,
2925
3056
  matchesSubmissionPageFilters,
2926
3057
  migrateSchemaTranslationMetadata,
3058
+ normalizeLocale,
2927
3059
  normalizeSubmissionPageSize,
2928
3060
  pipeResponsesToCsvStream,
2929
3061
  populateSchemaTranslations,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/core",
3
- "version": "4.6.0",
3
+ "version": "4.8.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },