@form-engine-ts/core 4.7.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
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 ? {} : {
@@ -2505,7 +2586,7 @@ var migrateSchemaTranslationMetadata = (schema, migratorOrOptions) => {
2505
2586
  (locale, property) => ({
2506
2587
  locale,
2507
2588
  defaultLocale,
2508
- path: property,
2589
+ path: `form.${property}`,
2509
2590
  property,
2510
2591
  nodeKind: "form"
2511
2592
  }),
@@ -2604,8 +2685,11 @@ function collectTranslationSlots(schema, locale) {
2604
2685
  return translationSlots(schema, locale).map((descriptor) => descriptor.slot);
2605
2686
  }
2606
2687
  function resolveLocalizedSchema(schema, targetLocale) {
2607
- if (targetLocale === void 0 || targetLocale.length === 0 || targetLocale === schema.defaultLocale) return schema;
2608
- 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);
2609
2693
  const completionMessage = formTranslation?.completionMessage ?? schema.completionMessage;
2610
2694
  return {
2611
2695
  ...schema,
@@ -2613,7 +2697,7 @@ function resolveLocalizedSchema(schema, targetLocale) {
2613
2697
  ...(formTranslation?.description ?? schema.description) === void 0 ? {} : { description: formTranslation?.description ?? schema.description },
2614
2698
  ...completionMessage === void 0 ? {} : { completionMessage },
2615
2699
  fields: schema.fields.map((field) => {
2616
- const translation = field.translations?.[targetLocale];
2700
+ const translation = localeRecordEntry2(field.translations, normalizedTargetLocale);
2617
2701
  const localized = {
2618
2702
  ...field,
2619
2703
  title: translation?.title ?? field.title,
@@ -2624,13 +2708,13 @@ function resolveLocalizedSchema(schema, targetLocale) {
2624
2708
  ...localized,
2625
2709
  options: field.options.map((option) => ({
2626
2710
  ...option,
2627
- label: option.translations?.[targetLocale] ?? option.label
2711
+ label: localeRecordEntry2(option.translations, normalizedTargetLocale) ?? option.label
2628
2712
  }))
2629
2713
  };
2630
2714
  }),
2631
2715
  ...schema.pages === void 0 ? {} : {
2632
2716
  pages: schema.pages.map((page) => {
2633
- const translation = page.translations?.[targetLocale];
2717
+ const translation = localeRecordEntry2(page.translations, normalizedTargetLocale);
2634
2718
  const title = translation?.title ?? page.title;
2635
2719
  const description = translation?.description ?? page.description;
2636
2720
  return {
@@ -2644,8 +2728,13 @@ function resolveLocalizedSchema(schema, targetLocale) {
2644
2728
  }
2645
2729
  async function populateSchemaTranslations(schema, targetLocales, adapter, options = {}) {
2646
2730
  assertValidFormSchema(schema);
2647
- const locales = [...new Set(targetLocales.filter((locale) => locale.length > 0 && locale !== schema.defaultLocale))];
2648
- 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);
2649
2738
  const collectedLocales = collectSchemaLocales(schema);
2650
2739
  const disallowedLocale = [...collectedLocales.allUniqueLocales, ...locales].find(
2651
2740
  (locale) => allowedLocales !== void 0 && !allowedLocales.includes(locale)
@@ -2687,7 +2776,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2687
2776
  adapter,
2688
2777
  selected.map((descriptor) => descriptor.slot.sourceText),
2689
2778
  locale,
2690
- schema.defaultLocale
2779
+ defaultLocale
2691
2780
  );
2692
2781
  if (translated.length !== selected.length) {
2693
2782
  throw new Error(`Translation adapter returned ${translated.length} texts for ${selected.length} inputs.`);
@@ -2696,7 +2785,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2696
2785
  const translatedText = translated[index];
2697
2786
  if (translatedText === void 0) throw new Error("Translation adapter returned an unexpected result.");
2698
2787
  const metadata = options.createMetadata?.(descriptor.slot, translatedText) ?? {
2699
- sourceLocale: schema.defaultLocale ?? "",
2788
+ sourceLocale: defaultLocale ?? "",
2700
2789
  sourceTextHash: computeSourceTextHash(descriptor.slot.sourceText),
2701
2790
  translationSource: "automatic",
2702
2791
  translatedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -2707,8 +2796,8 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2707
2796
  }
2708
2797
  const supportedLocales = [
2709
2798
  .../* @__PURE__ */ new Set([
2710
- ...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
2711
- ...schema.supportedLocales ?? [],
2799
+ ...defaultLocale === void 0 ? [] : [defaultLocale],
2800
+ ...(schema.supportedLocales ?? []).map((locale) => normalizeLocale(locale) ?? locale),
2712
2801
  ...locales
2713
2802
  ])
2714
2803
  ];
@@ -3044,6 +3133,7 @@ function assertVersionMutable(status) {
3044
3133
  matchesSubmissionFilter,
3045
3134
  matchesSubmissionPageFilters,
3046
3135
  migrateSchemaTranslationMetadata,
3136
+ normalizeLocale,
3047
3137
  normalizeSubmissionPageSize,
3048
3138
  pipeResponsesToCsvStream,
3049
3139
  populateSchemaTranslations,
package/dist/index.d.cts CHANGED
@@ -657,6 +657,14 @@ interface CreateSubmissionOptions extends ExtensibleNode {
657
657
  }
658
658
  declare function createSubmission(schema: FormSchema, values: FormValues, options: CreateSubmissionOptions): FormSubmission;
659
659
 
660
+ /**
661
+ * Normalizes a locale string to its BCP 47 canonical form.
662
+ *
663
+ * Underscore-separated locale tags are accepted for compatibility with common
664
+ * platform and user-input conventions. Invalid tags return null.
665
+ */
666
+ declare const normalizeLocale: (rawLocale: string) => string | null;
667
+
660
668
  interface TranslationSlot {
661
669
  readonly kind: "form" | "page" | "field" | "option";
662
670
  readonly nodeId: string;
@@ -768,4 +776,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
768
776
  declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
769
777
  declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
770
778
 
771
- export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type BaseFieldConstraintRule, type BuilderTranslationKey, type CanonicalTranslationMetadata, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceFieldConstraintRule, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, DEFAULT_FIELD_TYPE_DEFINITIONS, type DeleteDraftOptions, type DisplayCondition, type DisplayConditionGroup, type DisplayRule, type ExtensibleNode, type FieldConstraintRule, type FieldDisplayCondition, type FieldOption, type FieldType, type FieldTypeDefinition, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormSubmissionSettings, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LegacyTranslationMetadata, type LocalizedText, type MigrateSchemaTranslationMetadataOptions, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PaginationIteratorOptions, type PopulateTranslationOptions, type PopulateTranslationsOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type RatingFieldConstraintRule, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SanitizeSchemaOptions, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StorageCommitError, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionFilter, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextAnswerCursorValue, type TextAnswerItem, type TextAnswerPage, type TextAnswerPageQueryOptions, type TextField, type TextFieldConstraintRule, type TextQuestionAggregate, type TranslationAdapter, type TranslationMetadataMigrator, type TranslationMigrationContext, type TranslationReport, type TranslationSlot, type TranslationStatus, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionEvent, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, collectTranslationSlots, computeSourceTextHash, createCloneTransitionPlan, createDeleteDraftTransitionPlan, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, decodeTextAnswerCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, encodeTextAnswerCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, getTranslationStatus, isDisplayConditionGroupSatisfied, isDisplayConditionSatisfied, isManualTranslationMetadata, isQuestionVisible, iterateSubmissionPages, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, migrateSchemaTranslationMetadata, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, removeLocaleFromSchema, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
779
+ export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type BaseFieldConstraintRule, type BuilderTranslationKey, type CanonicalTranslationMetadata, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceFieldConstraintRule, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, DEFAULT_FIELD_TYPE_DEFINITIONS, type DeleteDraftOptions, type DisplayCondition, type DisplayConditionGroup, type DisplayRule, type ExtensibleNode, type FieldConstraintRule, type FieldDisplayCondition, type FieldOption, type FieldType, type FieldTypeDefinition, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormSubmissionSettings, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LegacyTranslationMetadata, type LocalizedText, type MigrateSchemaTranslationMetadataOptions, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PaginationIteratorOptions, type PopulateTranslationOptions, type PopulateTranslationsOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type RatingFieldConstraintRule, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SanitizeSchemaOptions, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StorageCommitError, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionFilter, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextAnswerCursorValue, type TextAnswerItem, type TextAnswerPage, type TextAnswerPageQueryOptions, type TextField, type TextFieldConstraintRule, type TextQuestionAggregate, type TranslationAdapter, type TranslationMetadataMigrator, type TranslationMigrationContext, type TranslationReport, type TranslationSlot, type TranslationStatus, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionEvent, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, collectTranslationSlots, computeSourceTextHash, createCloneTransitionPlan, createDeleteDraftTransitionPlan, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, decodeTextAnswerCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, encodeTextAnswerCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, getTranslationStatus, isDisplayConditionGroupSatisfied, isDisplayConditionSatisfied, isManualTranslationMetadata, isQuestionVisible, iterateSubmissionPages, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, migrateSchemaTranslationMetadata, normalizeLocale, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, removeLocaleFromSchema, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
package/dist/index.d.ts CHANGED
@@ -657,6 +657,14 @@ interface CreateSubmissionOptions extends ExtensibleNode {
657
657
  }
658
658
  declare function createSubmission(schema: FormSchema, values: FormValues, options: CreateSubmissionOptions): FormSubmission;
659
659
 
660
+ /**
661
+ * Normalizes a locale string to its BCP 47 canonical form.
662
+ *
663
+ * Underscore-separated locale tags are accepted for compatibility with common
664
+ * platform and user-input conventions. Invalid tags return null.
665
+ */
666
+ declare const normalizeLocale: (rawLocale: string) => string | null;
667
+
660
668
  interface TranslationSlot {
661
669
  readonly kind: "form" | "page" | "field" | "option";
662
670
  readonly nodeId: string;
@@ -768,4 +776,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
768
776
  declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
769
777
  declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
770
778
 
771
- export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type BaseFieldConstraintRule, type BuilderTranslationKey, type CanonicalTranslationMetadata, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceFieldConstraintRule, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, DEFAULT_FIELD_TYPE_DEFINITIONS, type DeleteDraftOptions, type DisplayCondition, type DisplayConditionGroup, type DisplayRule, type ExtensibleNode, type FieldConstraintRule, type FieldDisplayCondition, type FieldOption, type FieldType, type FieldTypeDefinition, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormSubmissionSettings, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LegacyTranslationMetadata, type LocalizedText, type MigrateSchemaTranslationMetadataOptions, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PaginationIteratorOptions, type PopulateTranslationOptions, type PopulateTranslationsOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type RatingFieldConstraintRule, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SanitizeSchemaOptions, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StorageCommitError, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionFilter, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextAnswerCursorValue, type TextAnswerItem, type TextAnswerPage, type TextAnswerPageQueryOptions, type TextField, type TextFieldConstraintRule, type TextQuestionAggregate, type TranslationAdapter, type TranslationMetadataMigrator, type TranslationMigrationContext, type TranslationReport, type TranslationSlot, type TranslationStatus, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionEvent, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, collectTranslationSlots, computeSourceTextHash, createCloneTransitionPlan, createDeleteDraftTransitionPlan, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, decodeTextAnswerCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, encodeTextAnswerCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, getTranslationStatus, isDisplayConditionGroupSatisfied, isDisplayConditionSatisfied, isManualTranslationMetadata, isQuestionVisible, iterateSubmissionPages, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, migrateSchemaTranslationMetadata, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, removeLocaleFromSchema, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
779
+ export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type BaseFieldConstraintRule, type BuilderTranslationKey, type CanonicalTranslationMetadata, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceFieldConstraintRule, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, DEFAULT_FIELD_TYPE_DEFINITIONS, type DeleteDraftOptions, type DisplayCondition, type DisplayConditionGroup, type DisplayRule, type ExtensibleNode, type FieldConstraintRule, type FieldDisplayCondition, type FieldOption, type FieldType, type FieldTypeDefinition, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormSubmissionSettings, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LegacyTranslationMetadata, type LocalizedText, type MigrateSchemaTranslationMetadataOptions, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PaginationIteratorOptions, type PopulateTranslationOptions, type PopulateTranslationsOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type RatingFieldConstraintRule, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SanitizeSchemaOptions, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StorageCommitError, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionFilter, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextAnswerCursorValue, type TextAnswerItem, type TextAnswerPage, type TextAnswerPageQueryOptions, type TextField, type TextFieldConstraintRule, type TextQuestionAggregate, type TranslationAdapter, type TranslationMetadataMigrator, type TranslationMigrationContext, type TranslationReport, type TranslationSlot, type TranslationStatus, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionEvent, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, collectTranslationSlots, computeSourceTextHash, createCloneTransitionPlan, createDeleteDraftTransitionPlan, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, decodeTextAnswerCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, encodeTextAnswerCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, getTranslationStatus, isDisplayConditionGroupSatisfied, isDisplayConditionSatisfied, isManualTranslationMetadata, isQuestionVisible, iterateSubmissionPages, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, migrateSchemaTranslationMetadata, normalizeLocale, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, removeLocaleFromSchema, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
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 ? {} : {
@@ -2429,7 +2509,7 @@ var migrateSchemaTranslationMetadata = (schema, migratorOrOptions) => {
2429
2509
  (locale, property) => ({
2430
2510
  locale,
2431
2511
  defaultLocale,
2432
- path: property,
2512
+ path: `form.${property}`,
2433
2513
  property,
2434
2514
  nodeKind: "form"
2435
2515
  }),
@@ -2528,8 +2608,11 @@ function collectTranslationSlots(schema, locale) {
2528
2608
  return translationSlots(schema, locale).map((descriptor) => descriptor.slot);
2529
2609
  }
2530
2610
  function resolveLocalizedSchema(schema, targetLocale) {
2531
- if (targetLocale === void 0 || targetLocale.length === 0 || targetLocale === schema.defaultLocale) return schema;
2532
- 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);
2533
2616
  const completionMessage = formTranslation?.completionMessage ?? schema.completionMessage;
2534
2617
  return {
2535
2618
  ...schema,
@@ -2537,7 +2620,7 @@ function resolveLocalizedSchema(schema, targetLocale) {
2537
2620
  ...(formTranslation?.description ?? schema.description) === void 0 ? {} : { description: formTranslation?.description ?? schema.description },
2538
2621
  ...completionMessage === void 0 ? {} : { completionMessage },
2539
2622
  fields: schema.fields.map((field) => {
2540
- const translation = field.translations?.[targetLocale];
2623
+ const translation = localeRecordEntry2(field.translations, normalizedTargetLocale);
2541
2624
  const localized = {
2542
2625
  ...field,
2543
2626
  title: translation?.title ?? field.title,
@@ -2548,13 +2631,13 @@ function resolveLocalizedSchema(schema, targetLocale) {
2548
2631
  ...localized,
2549
2632
  options: field.options.map((option) => ({
2550
2633
  ...option,
2551
- label: option.translations?.[targetLocale] ?? option.label
2634
+ label: localeRecordEntry2(option.translations, normalizedTargetLocale) ?? option.label
2552
2635
  }))
2553
2636
  };
2554
2637
  }),
2555
2638
  ...schema.pages === void 0 ? {} : {
2556
2639
  pages: schema.pages.map((page) => {
2557
- const translation = page.translations?.[targetLocale];
2640
+ const translation = localeRecordEntry2(page.translations, normalizedTargetLocale);
2558
2641
  const title = translation?.title ?? page.title;
2559
2642
  const description = translation?.description ?? page.description;
2560
2643
  return {
@@ -2568,8 +2651,13 @@ function resolveLocalizedSchema(schema, targetLocale) {
2568
2651
  }
2569
2652
  async function populateSchemaTranslations(schema, targetLocales, adapter, options = {}) {
2570
2653
  assertValidFormSchema(schema);
2571
- const locales = [...new Set(targetLocales.filter((locale) => locale.length > 0 && locale !== schema.defaultLocale))];
2572
- 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);
2573
2661
  const collectedLocales = collectSchemaLocales(schema);
2574
2662
  const disallowedLocale = [...collectedLocales.allUniqueLocales, ...locales].find(
2575
2663
  (locale) => allowedLocales !== void 0 && !allowedLocales.includes(locale)
@@ -2611,7 +2699,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2611
2699
  adapter,
2612
2700
  selected.map((descriptor) => descriptor.slot.sourceText),
2613
2701
  locale,
2614
- schema.defaultLocale
2702
+ defaultLocale
2615
2703
  );
2616
2704
  if (translated.length !== selected.length) {
2617
2705
  throw new Error(`Translation adapter returned ${translated.length} texts for ${selected.length} inputs.`);
@@ -2620,7 +2708,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2620
2708
  const translatedText = translated[index];
2621
2709
  if (translatedText === void 0) throw new Error("Translation adapter returned an unexpected result.");
2622
2710
  const metadata = options.createMetadata?.(descriptor.slot, translatedText) ?? {
2623
- sourceLocale: schema.defaultLocale ?? "",
2711
+ sourceLocale: defaultLocale ?? "",
2624
2712
  sourceTextHash: computeSourceTextHash(descriptor.slot.sourceText),
2625
2713
  translationSource: "automatic",
2626
2714
  translatedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -2631,8 +2719,8 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2631
2719
  }
2632
2720
  const supportedLocales = [
2633
2721
  .../* @__PURE__ */ new Set([
2634
- ...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
2635
- ...schema.supportedLocales ?? [],
2722
+ ...defaultLocale === void 0 ? [] : [defaultLocale],
2723
+ ...(schema.supportedLocales ?? []).map((locale) => normalizeLocale(locale) ?? locale),
2636
2724
  ...locales
2637
2725
  ])
2638
2726
  ];
@@ -2967,6 +3055,7 @@ export {
2967
3055
  matchesSubmissionFilter,
2968
3056
  matchesSubmissionPageFilters,
2969
3057
  migrateSchemaTranslationMetadata,
3058
+ normalizeLocale,
2970
3059
  normalizeSubmissionPageSize,
2971
3060
  pipeResponsesToCsvStream,
2972
3061
  populateSchemaTranslations,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/core",
3
- "version": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },