@form-engine-ts/core 4.5.0 → 4.6.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
@@ -62,6 +62,13 @@ operators include equality, containment, emptiness, and numeric comparisons; the
62
62
  and missing, translated, stale, or manual states for authoring tools. `populateSchemaTranslations` can populate stale
63
63
  and missing entries while preserving manual translations and reports skipped reasons.
64
64
 
65
+ Legacy translation metadata can be recognized with `isManualTranslationMetadata` and migrated with
66
+ `migrateSchemaTranslationMetadata`; pass a custom migrator when legacy fields need application-specific conversion.
67
+ `PopulateTranslationsOptions` is a compatibility alias for `PopulateTranslationOptions`, which also accepts custom
68
+ manual-translation detection and metadata normalization callbacks. `removeLocaleFromSchema(schema, locale)` removes
69
+ the locale from registrations and all form, page, field, option translation values and metadata; the default locale
70
+ cannot be removed.
71
+
65
72
  The React builder uses canonical keys with legacy aliases when resolving UI translations:
66
73
 
67
74
  | Canonical key | Legacy alias |
package/dist/index.cjs CHANGED
@@ -50,15 +50,18 @@ __export(index_exports, {
50
50
  getTranslationStatus: () => getTranslationStatus,
51
51
  isDisplayConditionGroupSatisfied: () => isDisplayConditionGroupSatisfied,
52
52
  isDisplayConditionSatisfied: () => isDisplayConditionSatisfied,
53
+ isManualTranslationMetadata: () => isManualTranslationMetadata,
53
54
  isQuestionVisible: () => isQuestionVisible,
54
55
  iterateSubmissionPages: () => iterateSubmissionPages,
55
56
  jsonValuesEqual: () => jsonValuesEqual,
56
57
  matchesSubmissionFilter: () => matchesSubmissionFilter,
57
58
  matchesSubmissionPageFilters: () => matchesSubmissionPageFilters,
59
+ migrateSchemaTranslationMetadata: () => migrateSchemaTranslationMetadata,
58
60
  normalizeSubmissionPageSize: () => normalizeSubmissionPageSize,
59
61
  pipeResponsesToCsvStream: () => pipeResponsesToCsvStream,
60
62
  populateSchemaTranslations: () => populateSchemaTranslations,
61
63
  publishDraft: () => publishDraft,
64
+ removeLocaleFromSchema: () => removeLocaleFromSchema,
62
65
  resolveFormTranslation: () => resolveFormTranslation,
63
66
  resolveLocalizedSchema: () => resolveLocalizedSchema,
64
67
  sanitizeSchema: () => sanitizeSchema,
@@ -2216,6 +2219,12 @@ function createSubmission(schema, values, options) {
2216
2219
  }
2217
2220
 
2218
2221
  // src/translation.ts
2222
+ var isManualTranslationMetadata = (metadata) => {
2223
+ if (metadata === void 0) return false;
2224
+ if ("isManuallyEdited" in metadata && metadata.isManuallyEdited === true || "isManual" in metadata && metadata.isManual === true)
2225
+ return true;
2226
+ return typeof metadata.translationSource === "string" && metadata.translationSource.toLowerCase() === "manual";
2227
+ };
2219
2228
  var computeSourceTextHash = (text) => {
2220
2229
  const normalized = text.normalize("NFKC").trim().replace(/\s+/gu, " ");
2221
2230
  let hash = 2166136261;
@@ -2226,11 +2235,14 @@ var computeSourceTextHash = (text) => {
2226
2235
  return (hash >>> 0).toString(16).padStart(8, "0");
2227
2236
  };
2228
2237
  function getTranslationStatus(sourceText, translatedText, metadata) {
2238
+ return getTranslationStatusWithManualOverride(sourceText, translatedText, metadata, void 0);
2239
+ }
2240
+ function getTranslationStatusWithManualOverride(sourceText, translatedText, metadata, manualOverride) {
2229
2241
  if (translatedText === void 0 || translatedText.trim() === "") return "missing";
2230
2242
  if (metadata === void 0) return "translated";
2231
2243
  const currentHash = computeSourceTextHash(sourceText);
2232
2244
  const sourceHash = typeof metadata.sourceTextHash === "string" ? metadata.sourceTextHash : void 0;
2233
- const isManual = metadata.translationSource === "manual" || "isManual" in metadata && metadata.isManual === true;
2245
+ const isManual = manualOverride ?? isManualTranslationMetadata(metadata);
2234
2246
  if (isManual) return sourceHash === void 0 || sourceHash === currentHash ? "manual" : "manual-stale";
2235
2247
  return sourceHash === void 0 || sourceHash === currentHash ? "translated" : "stale";
2236
2248
  }
@@ -2256,8 +2268,11 @@ function withTranslationMetadata(node, locale, property, metadata) {
2256
2268
  }
2257
2269
  };
2258
2270
  }
2259
- function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata) {
2260
- const status = getTranslationStatus(sourceText, existingText, existingTranslationMetadata);
2271
+ function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata, options = {}) {
2272
+ const path = `${kind}.${nodeId}.${property}`;
2273
+ const manual = options.isManualTranslation?.(existingTranslationMetadata, { path, locale }) ?? isManualTranslationMetadata(existingTranslationMetadata);
2274
+ const normalizedMetadata = existingTranslationMetadata === void 0 ? void 0 : options.normalizeMetadata === void 0 ? existingTranslationMetadata : { ...options.normalizeMetadata(existingTranslationMetadata, sourceText) };
2275
+ const status = getTranslationStatusWithManualOverride(sourceText, existingText, normalizedMetadata, manual);
2261
2276
  return {
2262
2277
  kind,
2263
2278
  nodeId,
@@ -2265,15 +2280,15 @@ function createSlot(kind, nodeId, property, locale, sourceText, existingText, no
2265
2280
  locale,
2266
2281
  sourceText,
2267
2282
  target: { kind, ...nodeId.length === 0 ? {} : { id: nodeId }, property },
2268
- path: `${kind}.${nodeId}.${property}`,
2283
+ path,
2269
2284
  sourceTextHash: computeSourceTextHash(sourceText),
2270
2285
  status,
2271
2286
  ...existingText === void 0 ? {} : { existingText },
2272
2287
  ...nodeMetadata === void 0 ? {} : { nodeMetadata, metadata: nodeMetadata },
2273
- ...existingTranslationMetadata === void 0 ? {} : { existingTranslationMetadata }
2288
+ ...normalizedMetadata === void 0 ? {} : { existingTranslationMetadata: normalizedMetadata }
2274
2289
  };
2275
2290
  }
2276
- function translationSlots(schema, locale) {
2291
+ function translationSlots(schema, locale, options = {}) {
2277
2292
  const descriptors = [];
2278
2293
  const addFormSlot = (property, sourceText) => {
2279
2294
  const slot = createSlot(
@@ -2284,10 +2299,15 @@ function translationSlots(schema, locale) {
2284
2299
  sourceText,
2285
2300
  schema.translations?.[locale]?.[property],
2286
2301
  schema.metadata,
2287
- schema.translationMetadata?.[locale]?.[property]
2302
+ schema.translationMetadata?.[locale]?.[property],
2303
+ options
2288
2304
  );
2289
2305
  descriptors.push({
2290
2306
  slot,
2307
+ manual: options.isManualTranslation?.(schema.translationMetadata?.[locale]?.[property], {
2308
+ path: slot.path ?? "",
2309
+ locale
2310
+ }) ?? isManualTranslationMetadata(schema.translationMetadata?.[locale]?.[property]),
2291
2311
  apply: (current, value, metadata) => withTranslationMetadata(
2292
2312
  { ...current, translations: mergeLocalizedText(current.translations, locale, property, value) },
2293
2313
  locale,
@@ -2309,10 +2329,15 @@ function translationSlots(schema, locale) {
2309
2329
  sourceText,
2310
2330
  field.translations?.[locale]?.[property],
2311
2331
  field.metadata,
2312
- field.translationMetadata?.[locale]?.[property]
2332
+ field.translationMetadata?.[locale]?.[property],
2333
+ options
2313
2334
  );
2314
2335
  descriptors.push({
2315
2336
  slot,
2337
+ manual: options.isManualTranslation?.(field.translationMetadata?.[locale]?.[property], {
2338
+ path: slot.path ?? "",
2339
+ locale
2340
+ }) ?? isManualTranslationMetadata(field.translationMetadata?.[locale]?.[property]),
2316
2341
  apply: (current, value, metadata) => ({
2317
2342
  ...current,
2318
2343
  fields: current.fields.map(
@@ -2341,10 +2366,15 @@ function translationSlots(schema, locale) {
2341
2366
  option.label,
2342
2367
  option.translations?.[locale],
2343
2368
  option.metadata,
2344
- option.translationMetadata?.[locale]?.label
2369
+ option.translationMetadata?.[locale]?.label,
2370
+ options
2345
2371
  );
2346
2372
  descriptors.push({
2347
2373
  slot,
2374
+ manual: options.isManualTranslation?.(option.translationMetadata?.[locale]?.label, {
2375
+ path: slot.path ?? "",
2376
+ locale
2377
+ }) ?? isManualTranslationMetadata(option.translationMetadata?.[locale]?.label),
2348
2378
  apply: (current, value, metadata) => ({
2349
2379
  ...current,
2350
2380
  fields: current.fields.map((candidate, candidateIndex) => {
@@ -2379,10 +2409,15 @@ function translationSlots(schema, locale) {
2379
2409
  sourceText,
2380
2410
  page.translations?.[locale]?.[property],
2381
2411
  page.metadata,
2382
- page.translationMetadata?.[locale]?.[property]
2412
+ page.translationMetadata?.[locale]?.[property],
2413
+ options
2383
2414
  );
2384
2415
  descriptors.push({
2385
2416
  slot,
2417
+ manual: options.isManualTranslation?.(page.translationMetadata?.[locale]?.[property], {
2418
+ path: slot.path ?? "",
2419
+ locale
2420
+ }) ?? isManualTranslationMetadata(page.translationMetadata?.[locale]?.[property]),
2386
2421
  apply: (current, value, metadata) => ({
2387
2422
  ...current,
2388
2423
  ...current.pages === void 0 ? {} : {
@@ -2406,6 +2441,122 @@ function translationSlots(schema, locale) {
2406
2441
  });
2407
2442
  return descriptors;
2408
2443
  }
2444
+ function removeLocaleRecord(record, locale) {
2445
+ if (record === void 0) return void 0;
2446
+ const remaining = Object.entries(record).filter(([candidate]) => candidate !== locale);
2447
+ return remaining.length === 0 ? void 0 : Object.fromEntries(remaining);
2448
+ }
2449
+ function removeLocalizedNodeLocale(node, locale) {
2450
+ const translations = removeLocaleRecord(node.translations, locale);
2451
+ const translationMetadata = removeLocaleRecord(node.translationMetadata, locale);
2452
+ const { translations: _translations, translationMetadata: _translationMetadata, ...base } = node;
2453
+ return {
2454
+ ...base,
2455
+ ...translations === void 0 ? {} : { translations },
2456
+ ...translationMetadata === void 0 ? {} : { translationMetadata }
2457
+ };
2458
+ }
2459
+ function migrateMetadata(metadata, sourceText, defaultLocale, customMigrator) {
2460
+ if (customMigrator !== void 0) return customMigrator(metadata, sourceText);
2461
+ const record = metadata !== null && typeof metadata === "object" ? metadata : void 0;
2462
+ const sourceLocale = typeof record?.sourceLocale === "string" ? record.sourceLocale : defaultLocale ?? "";
2463
+ const translationSource = isManualTranslationMetadata(record) ? "manual" : "automatic";
2464
+ return {
2465
+ sourceLocale,
2466
+ sourceTextHash: computeSourceTextHash(sourceText),
2467
+ translationSource,
2468
+ ...typeof record?.translatedAt === "string" ? { translatedAt: record.translatedAt } : {},
2469
+ ...typeof record?.editedAt === "string" ? { editedAt: record.editedAt } : {}
2470
+ };
2471
+ }
2472
+ function migrateNodeMetadata(node, sourceTexts, defaultLocale, customMigrator) {
2473
+ if (node.translationMetadata === void 0) return node;
2474
+ const translationMetadata = Object.fromEntries(
2475
+ Object.entries(node.translationMetadata).map(([locale, properties]) => [
2476
+ locale,
2477
+ Object.fromEntries(
2478
+ Object.entries(properties).map(([property, metadata]) => [
2479
+ property,
2480
+ migrateMetadata(metadata, sourceTexts[property] ?? "", defaultLocale, customMigrator)
2481
+ ])
2482
+ )
2483
+ ])
2484
+ );
2485
+ return { ...node, translationMetadata };
2486
+ }
2487
+ var migrateSchemaTranslationMetadata = (schema, customMigrator) => {
2488
+ const migratedSchema = migrateNodeMetadata(
2489
+ schema,
2490
+ {
2491
+ title: schema.title,
2492
+ ...schema.description === void 0 ? {} : { description: schema.description },
2493
+ ...schema.completionMessage === void 0 ? {} : { completionMessage: schema.completionMessage }
2494
+ },
2495
+ schema.defaultLocale,
2496
+ customMigrator
2497
+ );
2498
+ const fields = schema.fields.map((field) => {
2499
+ const migratedField = migrateNodeMetadata(
2500
+ field,
2501
+ { title: field.title, ...field.description === void 0 ? {} : { description: field.description } },
2502
+ schema.defaultLocale,
2503
+ customMigrator
2504
+ );
2505
+ if (!("options" in migratedField)) return migratedField;
2506
+ return {
2507
+ ...migratedField,
2508
+ options: migratedField.options.map(
2509
+ (option) => migrateNodeMetadata(option, { label: option.label }, schema.defaultLocale, customMigrator)
2510
+ )
2511
+ };
2512
+ });
2513
+ const pages = schema.pages?.map(
2514
+ (page) => migrateNodeMetadata(
2515
+ page,
2516
+ {
2517
+ ...page.title === void 0 ? {} : { title: page.title },
2518
+ ...page.description === void 0 ? {} : { description: page.description }
2519
+ },
2520
+ schema.defaultLocale,
2521
+ customMigrator
2522
+ )
2523
+ );
2524
+ return {
2525
+ ...migratedSchema,
2526
+ fields,
2527
+ ...pages === void 0 ? {} : { pages }
2528
+ };
2529
+ };
2530
+ var removeLocaleFromSchema = (schema, localeToRemove) => {
2531
+ if (localeToRemove === schema.defaultLocale) {
2532
+ throw new Error(`Cannot remove defaultLocale: ${localeToRemove}`);
2533
+ }
2534
+ const form = removeLocalizedNodeLocale(schema, localeToRemove);
2535
+ const fields = schema.fields.map((field) => {
2536
+ const localizedField = removeLocalizedNodeLocale(field, localeToRemove);
2537
+ if (!("options" in localizedField)) return localizedField;
2538
+ return {
2539
+ ...localizedField,
2540
+ options: localizedField.options.map((option) => {
2541
+ const translations = removeLocaleRecord(option.translations, localeToRemove);
2542
+ const translationMetadata = removeLocaleRecord(option.translationMetadata, localeToRemove);
2543
+ const { translations: _translations, translationMetadata: _translationMetadata, ...base } = option;
2544
+ return {
2545
+ ...base,
2546
+ ...translations === void 0 ? {} : { translations },
2547
+ ...translationMetadata === void 0 ? {} : { translationMetadata }
2548
+ };
2549
+ })
2550
+ };
2551
+ });
2552
+ const pages = schema.pages?.map((page) => removeLocalizedNodeLocale(page, localeToRemove));
2553
+ return {
2554
+ ...form,
2555
+ supportedLocales: (schema.supportedLocales ?? []).filter((locale) => locale !== localeToRemove),
2556
+ fields,
2557
+ ...pages === void 0 ? {} : { pages }
2558
+ };
2559
+ };
2409
2560
  function collectTranslationSlots(schema, locale) {
2410
2561
  return translationSlots(schema, locale).map((descriptor) => descriptor.slot);
2411
2562
  }
@@ -2469,7 +2620,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2469
2620
  const skippedReasons = {};
2470
2621
  let result = schema;
2471
2622
  for (const locale of locales) {
2472
- const descriptors = translationSlots(schema, locale);
2623
+ const descriptors = translationSlots(schema, locale, options);
2473
2624
  if (options.markStaleTranslations ?? true) {
2474
2625
  for (const descriptor of descriptors) {
2475
2626
  if (descriptor.slot.status === "stale" || descriptor.slot.status === "manual-stale")
@@ -2479,8 +2630,9 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2479
2630
  const selected = [];
2480
2631
  for (const descriptor of descriptors) {
2481
2632
  const status = descriptor.slot.status ?? "missing";
2482
- const manual = status === "manual" || status === "manual-stale";
2483
- const shouldTranslate = options.shouldOverwrite?.(descriptor.slot) ?? ((!(options.preserveManualTranslations ?? true) || !manual) && (options.overwrite === "all" || (options.overwrite === "stale-and-missing" || options.overwrite === void 0 ? status === "missing" || status === "stale" || !(options.preserveManualTranslations ?? true) && status === "manual-stale" : status === "missing")));
2633
+ const manual = descriptor.manual || status === "manual" || status === "manual-stale";
2634
+ const requestedOverwrite = options.shouldOverwrite?.(descriptor.slot);
2635
+ const shouldTranslate = (options.preserveManualTranslations ?? true) && manual ? false : requestedOverwrite ?? (options.overwrite === "all" || (options.overwrite === "stale-and-missing" || options.overwrite === void 0 ? status === "missing" || status === "stale" || !(options.preserveManualTranslations ?? true) && status === "manual-stale" : status === "missing"));
2484
2636
  if (shouldTranslate) selected.push(descriptor);
2485
2637
  else {
2486
2638
  skippedSlots.push(descriptor.slot);
@@ -2842,15 +2994,18 @@ function assertVersionMutable(status) {
2842
2994
  getTranslationStatus,
2843
2995
  isDisplayConditionGroupSatisfied,
2844
2996
  isDisplayConditionSatisfied,
2997
+ isManualTranslationMetadata,
2845
2998
  isQuestionVisible,
2846
2999
  iterateSubmissionPages,
2847
3000
  jsonValuesEqual,
2848
3001
  matchesSubmissionFilter,
2849
3002
  matchesSubmissionPageFilters,
3003
+ migrateSchemaTranslationMetadata,
2850
3004
  normalizeSubmissionPageSize,
2851
3005
  pipeResponsesToCsvStream,
2852
3006
  populateSchemaTranslations,
2853
3007
  publishDraft,
3008
+ removeLocaleFromSchema,
2854
3009
  resolveFormTranslation,
2855
3010
  resolveLocalizedSchema,
2856
3011
  sanitizeSchema,
package/dist/index.d.cts CHANGED
@@ -686,15 +686,34 @@ interface CanonicalTranslationMetadata {
686
686
  readonly translatedAt?: string;
687
687
  readonly editedAt?: string;
688
688
  }
689
+ interface LegacyTranslationMetadata {
690
+ readonly isManuallyEdited?: boolean;
691
+ readonly translationSource?: "MANUAL" | "AUTOMATIC" | "manual" | "automatic" | string;
692
+ readonly sourceTextHash?: string;
693
+ readonly sourceText?: string;
694
+ readonly sourceLocale?: string;
695
+ readonly translatedAt?: string;
696
+ readonly editedAt?: string;
697
+ readonly isManual?: boolean;
698
+ readonly [key: string]: unknown;
699
+ }
700
+ declare const isManualTranslationMetadata: (metadata?: LegacyTranslationMetadata | CanonicalTranslationMetadata) => boolean;
689
701
  interface PopulateTranslationOptions {
690
702
  readonly overwrite?: "all" | "missing-only" | "stale-and-missing";
691
703
  readonly preserveManualTranslations?: boolean;
692
704
  readonly markStaleTranslations?: boolean;
693
705
  readonly shouldOverwrite?: (slot: TranslationSlot) => boolean;
694
706
  readonly createMetadata?: (slot: TranslationSlot, translatedText: string) => Readonly<Record<string, JsonValue>>;
707
+ readonly isManualTranslation?: (metadata: unknown, context: {
708
+ readonly path: string;
709
+ readonly locale: string;
710
+ }) => boolean;
711
+ readonly normalizeMetadata?: (metadata: unknown, sourceText: string) => CanonicalTranslationMetadata;
695
712
  /** Applies locale admission and count limits before the adapter is called. */
696
713
  readonly policy?: Pick<FormPolicy, "allowedLocales" | "maxLocales">;
697
714
  }
715
+ /** Compatibility alias for clients that used the pluralized options name. */
716
+ type PopulateTranslationsOptions = PopulateTranslationOptions;
698
717
  interface TranslationReport {
699
718
  readonly updatedSlots: readonly TranslationSlot[];
700
719
  readonly skippedSlots: readonly TranslationSlot[];
@@ -703,6 +722,9 @@ interface TranslationReport {
703
722
  }
704
723
  declare const computeSourceTextHash: (text: string) => string;
705
724
  declare function getTranslationStatus(sourceText: string, translatedText: string | undefined, metadata: CanonicalTranslationMetadata | Readonly<Record<string, JsonValue>> | undefined): TranslationStatus;
725
+ declare const migrateSchemaTranslationMetadata: (schema: FormSchema, customMigrator?: (oldMeta: unknown, sourceText: string) => CanonicalTranslationMetadata) => FormSchema;
726
+ /** Removes a locale registration and every localized value and metadata entry for it. */
727
+ declare const removeLocaleFromSchema: (schema: FormSchema, localeToRemove: string) => FormSchema;
706
728
  declare function collectTranslationSlots(schema: FormSchema, locale: string): readonly TranslationSlot[];
707
729
  declare function resolveLocalizedSchema(schema: FormSchema, targetLocale?: string): FormSchema;
708
730
  declare function populateSchemaTranslations(schema: FormSchema, targetLocales: readonly string[], adapter: AsyncTranslationAdapter, options?: PopulateTranslationOptions): Promise<{
@@ -725,4 +747,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
725
747
  declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
726
748
  declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
727
749
 
728
- 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 LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PaginationIteratorOptions, type PopulateTranslationOptions, 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 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, isQuestionVisible, iterateSubmissionPages, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
750
+ 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 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 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 };
package/dist/index.d.ts CHANGED
@@ -686,15 +686,34 @@ interface CanonicalTranslationMetadata {
686
686
  readonly translatedAt?: string;
687
687
  readonly editedAt?: string;
688
688
  }
689
+ interface LegacyTranslationMetadata {
690
+ readonly isManuallyEdited?: boolean;
691
+ readonly translationSource?: "MANUAL" | "AUTOMATIC" | "manual" | "automatic" | string;
692
+ readonly sourceTextHash?: string;
693
+ readonly sourceText?: string;
694
+ readonly sourceLocale?: string;
695
+ readonly translatedAt?: string;
696
+ readonly editedAt?: string;
697
+ readonly isManual?: boolean;
698
+ readonly [key: string]: unknown;
699
+ }
700
+ declare const isManualTranslationMetadata: (metadata?: LegacyTranslationMetadata | CanonicalTranslationMetadata) => boolean;
689
701
  interface PopulateTranslationOptions {
690
702
  readonly overwrite?: "all" | "missing-only" | "stale-and-missing";
691
703
  readonly preserveManualTranslations?: boolean;
692
704
  readonly markStaleTranslations?: boolean;
693
705
  readonly shouldOverwrite?: (slot: TranslationSlot) => boolean;
694
706
  readonly createMetadata?: (slot: TranslationSlot, translatedText: string) => Readonly<Record<string, JsonValue>>;
707
+ readonly isManualTranslation?: (metadata: unknown, context: {
708
+ readonly path: string;
709
+ readonly locale: string;
710
+ }) => boolean;
711
+ readonly normalizeMetadata?: (metadata: unknown, sourceText: string) => CanonicalTranslationMetadata;
695
712
  /** Applies locale admission and count limits before the adapter is called. */
696
713
  readonly policy?: Pick<FormPolicy, "allowedLocales" | "maxLocales">;
697
714
  }
715
+ /** Compatibility alias for clients that used the pluralized options name. */
716
+ type PopulateTranslationsOptions = PopulateTranslationOptions;
698
717
  interface TranslationReport {
699
718
  readonly updatedSlots: readonly TranslationSlot[];
700
719
  readonly skippedSlots: readonly TranslationSlot[];
@@ -703,6 +722,9 @@ interface TranslationReport {
703
722
  }
704
723
  declare const computeSourceTextHash: (text: string) => string;
705
724
  declare function getTranslationStatus(sourceText: string, translatedText: string | undefined, metadata: CanonicalTranslationMetadata | Readonly<Record<string, JsonValue>> | undefined): TranslationStatus;
725
+ declare const migrateSchemaTranslationMetadata: (schema: FormSchema, customMigrator?: (oldMeta: unknown, sourceText: string) => CanonicalTranslationMetadata) => FormSchema;
726
+ /** Removes a locale registration and every localized value and metadata entry for it. */
727
+ declare const removeLocaleFromSchema: (schema: FormSchema, localeToRemove: string) => FormSchema;
706
728
  declare function collectTranslationSlots(schema: FormSchema, locale: string): readonly TranslationSlot[];
707
729
  declare function resolveLocalizedSchema(schema: FormSchema, targetLocale?: string): FormSchema;
708
730
  declare function populateSchemaTranslations(schema: FormSchema, targetLocales: readonly string[], adapter: AsyncTranslationAdapter, options?: PopulateTranslationOptions): Promise<{
@@ -725,4 +747,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
725
747
  declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
726
748
  declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
727
749
 
728
- 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 LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PaginationIteratorOptions, type PopulateTranslationOptions, 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 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, isQuestionVisible, iterateSubmissionPages, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
750
+ 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 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 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 };
package/dist/index.js CHANGED
@@ -2143,6 +2143,12 @@ function createSubmission(schema, values, options) {
2143
2143
  }
2144
2144
 
2145
2145
  // src/translation.ts
2146
+ var isManualTranslationMetadata = (metadata) => {
2147
+ if (metadata === void 0) return false;
2148
+ if ("isManuallyEdited" in metadata && metadata.isManuallyEdited === true || "isManual" in metadata && metadata.isManual === true)
2149
+ return true;
2150
+ return typeof metadata.translationSource === "string" && metadata.translationSource.toLowerCase() === "manual";
2151
+ };
2146
2152
  var computeSourceTextHash = (text) => {
2147
2153
  const normalized = text.normalize("NFKC").trim().replace(/\s+/gu, " ");
2148
2154
  let hash = 2166136261;
@@ -2153,11 +2159,14 @@ var computeSourceTextHash = (text) => {
2153
2159
  return (hash >>> 0).toString(16).padStart(8, "0");
2154
2160
  };
2155
2161
  function getTranslationStatus(sourceText, translatedText, metadata) {
2162
+ return getTranslationStatusWithManualOverride(sourceText, translatedText, metadata, void 0);
2163
+ }
2164
+ function getTranslationStatusWithManualOverride(sourceText, translatedText, metadata, manualOverride) {
2156
2165
  if (translatedText === void 0 || translatedText.trim() === "") return "missing";
2157
2166
  if (metadata === void 0) return "translated";
2158
2167
  const currentHash = computeSourceTextHash(sourceText);
2159
2168
  const sourceHash = typeof metadata.sourceTextHash === "string" ? metadata.sourceTextHash : void 0;
2160
- const isManual = metadata.translationSource === "manual" || "isManual" in metadata && metadata.isManual === true;
2169
+ const isManual = manualOverride ?? isManualTranslationMetadata(metadata);
2161
2170
  if (isManual) return sourceHash === void 0 || sourceHash === currentHash ? "manual" : "manual-stale";
2162
2171
  return sourceHash === void 0 || sourceHash === currentHash ? "translated" : "stale";
2163
2172
  }
@@ -2183,8 +2192,11 @@ function withTranslationMetadata(node, locale, property, metadata) {
2183
2192
  }
2184
2193
  };
2185
2194
  }
2186
- function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata) {
2187
- const status = getTranslationStatus(sourceText, existingText, existingTranslationMetadata);
2195
+ function createSlot(kind, nodeId, property, locale, sourceText, existingText, nodeMetadata, existingTranslationMetadata, options = {}) {
2196
+ const path = `${kind}.${nodeId}.${property}`;
2197
+ const manual = options.isManualTranslation?.(existingTranslationMetadata, { path, locale }) ?? isManualTranslationMetadata(existingTranslationMetadata);
2198
+ const normalizedMetadata = existingTranslationMetadata === void 0 ? void 0 : options.normalizeMetadata === void 0 ? existingTranslationMetadata : { ...options.normalizeMetadata(existingTranslationMetadata, sourceText) };
2199
+ const status = getTranslationStatusWithManualOverride(sourceText, existingText, normalizedMetadata, manual);
2188
2200
  return {
2189
2201
  kind,
2190
2202
  nodeId,
@@ -2192,15 +2204,15 @@ function createSlot(kind, nodeId, property, locale, sourceText, existingText, no
2192
2204
  locale,
2193
2205
  sourceText,
2194
2206
  target: { kind, ...nodeId.length === 0 ? {} : { id: nodeId }, property },
2195
- path: `${kind}.${nodeId}.${property}`,
2207
+ path,
2196
2208
  sourceTextHash: computeSourceTextHash(sourceText),
2197
2209
  status,
2198
2210
  ...existingText === void 0 ? {} : { existingText },
2199
2211
  ...nodeMetadata === void 0 ? {} : { nodeMetadata, metadata: nodeMetadata },
2200
- ...existingTranslationMetadata === void 0 ? {} : { existingTranslationMetadata }
2212
+ ...normalizedMetadata === void 0 ? {} : { existingTranslationMetadata: normalizedMetadata }
2201
2213
  };
2202
2214
  }
2203
- function translationSlots(schema, locale) {
2215
+ function translationSlots(schema, locale, options = {}) {
2204
2216
  const descriptors = [];
2205
2217
  const addFormSlot = (property, sourceText) => {
2206
2218
  const slot = createSlot(
@@ -2211,10 +2223,15 @@ function translationSlots(schema, locale) {
2211
2223
  sourceText,
2212
2224
  schema.translations?.[locale]?.[property],
2213
2225
  schema.metadata,
2214
- schema.translationMetadata?.[locale]?.[property]
2226
+ schema.translationMetadata?.[locale]?.[property],
2227
+ options
2215
2228
  );
2216
2229
  descriptors.push({
2217
2230
  slot,
2231
+ manual: options.isManualTranslation?.(schema.translationMetadata?.[locale]?.[property], {
2232
+ path: slot.path ?? "",
2233
+ locale
2234
+ }) ?? isManualTranslationMetadata(schema.translationMetadata?.[locale]?.[property]),
2218
2235
  apply: (current, value, metadata) => withTranslationMetadata(
2219
2236
  { ...current, translations: mergeLocalizedText(current.translations, locale, property, value) },
2220
2237
  locale,
@@ -2236,10 +2253,15 @@ function translationSlots(schema, locale) {
2236
2253
  sourceText,
2237
2254
  field.translations?.[locale]?.[property],
2238
2255
  field.metadata,
2239
- field.translationMetadata?.[locale]?.[property]
2256
+ field.translationMetadata?.[locale]?.[property],
2257
+ options
2240
2258
  );
2241
2259
  descriptors.push({
2242
2260
  slot,
2261
+ manual: options.isManualTranslation?.(field.translationMetadata?.[locale]?.[property], {
2262
+ path: slot.path ?? "",
2263
+ locale
2264
+ }) ?? isManualTranslationMetadata(field.translationMetadata?.[locale]?.[property]),
2243
2265
  apply: (current, value, metadata) => ({
2244
2266
  ...current,
2245
2267
  fields: current.fields.map(
@@ -2268,10 +2290,15 @@ function translationSlots(schema, locale) {
2268
2290
  option.label,
2269
2291
  option.translations?.[locale],
2270
2292
  option.metadata,
2271
- option.translationMetadata?.[locale]?.label
2293
+ option.translationMetadata?.[locale]?.label,
2294
+ options
2272
2295
  );
2273
2296
  descriptors.push({
2274
2297
  slot,
2298
+ manual: options.isManualTranslation?.(option.translationMetadata?.[locale]?.label, {
2299
+ path: slot.path ?? "",
2300
+ locale
2301
+ }) ?? isManualTranslationMetadata(option.translationMetadata?.[locale]?.label),
2275
2302
  apply: (current, value, metadata) => ({
2276
2303
  ...current,
2277
2304
  fields: current.fields.map((candidate, candidateIndex) => {
@@ -2306,10 +2333,15 @@ function translationSlots(schema, locale) {
2306
2333
  sourceText,
2307
2334
  page.translations?.[locale]?.[property],
2308
2335
  page.metadata,
2309
- page.translationMetadata?.[locale]?.[property]
2336
+ page.translationMetadata?.[locale]?.[property],
2337
+ options
2310
2338
  );
2311
2339
  descriptors.push({
2312
2340
  slot,
2341
+ manual: options.isManualTranslation?.(page.translationMetadata?.[locale]?.[property], {
2342
+ path: slot.path ?? "",
2343
+ locale
2344
+ }) ?? isManualTranslationMetadata(page.translationMetadata?.[locale]?.[property]),
2313
2345
  apply: (current, value, metadata) => ({
2314
2346
  ...current,
2315
2347
  ...current.pages === void 0 ? {} : {
@@ -2333,6 +2365,122 @@ function translationSlots(schema, locale) {
2333
2365
  });
2334
2366
  return descriptors;
2335
2367
  }
2368
+ function removeLocaleRecord(record, locale) {
2369
+ if (record === void 0) return void 0;
2370
+ const remaining = Object.entries(record).filter(([candidate]) => candidate !== locale);
2371
+ return remaining.length === 0 ? void 0 : Object.fromEntries(remaining);
2372
+ }
2373
+ function removeLocalizedNodeLocale(node, locale) {
2374
+ const translations = removeLocaleRecord(node.translations, locale);
2375
+ const translationMetadata = removeLocaleRecord(node.translationMetadata, locale);
2376
+ const { translations: _translations, translationMetadata: _translationMetadata, ...base } = node;
2377
+ return {
2378
+ ...base,
2379
+ ...translations === void 0 ? {} : { translations },
2380
+ ...translationMetadata === void 0 ? {} : { translationMetadata }
2381
+ };
2382
+ }
2383
+ function migrateMetadata(metadata, sourceText, defaultLocale, customMigrator) {
2384
+ if (customMigrator !== void 0) return customMigrator(metadata, sourceText);
2385
+ const record = metadata !== null && typeof metadata === "object" ? metadata : void 0;
2386
+ const sourceLocale = typeof record?.sourceLocale === "string" ? record.sourceLocale : defaultLocale ?? "";
2387
+ const translationSource = isManualTranslationMetadata(record) ? "manual" : "automatic";
2388
+ return {
2389
+ sourceLocale,
2390
+ sourceTextHash: computeSourceTextHash(sourceText),
2391
+ translationSource,
2392
+ ...typeof record?.translatedAt === "string" ? { translatedAt: record.translatedAt } : {},
2393
+ ...typeof record?.editedAt === "string" ? { editedAt: record.editedAt } : {}
2394
+ };
2395
+ }
2396
+ function migrateNodeMetadata(node, sourceTexts, defaultLocale, customMigrator) {
2397
+ if (node.translationMetadata === void 0) return node;
2398
+ const translationMetadata = Object.fromEntries(
2399
+ Object.entries(node.translationMetadata).map(([locale, properties]) => [
2400
+ locale,
2401
+ Object.fromEntries(
2402
+ Object.entries(properties).map(([property, metadata]) => [
2403
+ property,
2404
+ migrateMetadata(metadata, sourceTexts[property] ?? "", defaultLocale, customMigrator)
2405
+ ])
2406
+ )
2407
+ ])
2408
+ );
2409
+ return { ...node, translationMetadata };
2410
+ }
2411
+ var migrateSchemaTranslationMetadata = (schema, customMigrator) => {
2412
+ const migratedSchema = migrateNodeMetadata(
2413
+ schema,
2414
+ {
2415
+ title: schema.title,
2416
+ ...schema.description === void 0 ? {} : { description: schema.description },
2417
+ ...schema.completionMessage === void 0 ? {} : { completionMessage: schema.completionMessage }
2418
+ },
2419
+ schema.defaultLocale,
2420
+ customMigrator
2421
+ );
2422
+ const fields = schema.fields.map((field) => {
2423
+ const migratedField = migrateNodeMetadata(
2424
+ field,
2425
+ { title: field.title, ...field.description === void 0 ? {} : { description: field.description } },
2426
+ schema.defaultLocale,
2427
+ customMigrator
2428
+ );
2429
+ if (!("options" in migratedField)) return migratedField;
2430
+ return {
2431
+ ...migratedField,
2432
+ options: migratedField.options.map(
2433
+ (option) => migrateNodeMetadata(option, { label: option.label }, schema.defaultLocale, customMigrator)
2434
+ )
2435
+ };
2436
+ });
2437
+ const pages = schema.pages?.map(
2438
+ (page) => migrateNodeMetadata(
2439
+ page,
2440
+ {
2441
+ ...page.title === void 0 ? {} : { title: page.title },
2442
+ ...page.description === void 0 ? {} : { description: page.description }
2443
+ },
2444
+ schema.defaultLocale,
2445
+ customMigrator
2446
+ )
2447
+ );
2448
+ return {
2449
+ ...migratedSchema,
2450
+ fields,
2451
+ ...pages === void 0 ? {} : { pages }
2452
+ };
2453
+ };
2454
+ var removeLocaleFromSchema = (schema, localeToRemove) => {
2455
+ if (localeToRemove === schema.defaultLocale) {
2456
+ throw new Error(`Cannot remove defaultLocale: ${localeToRemove}`);
2457
+ }
2458
+ const form = removeLocalizedNodeLocale(schema, localeToRemove);
2459
+ const fields = schema.fields.map((field) => {
2460
+ const localizedField = removeLocalizedNodeLocale(field, localeToRemove);
2461
+ if (!("options" in localizedField)) return localizedField;
2462
+ return {
2463
+ ...localizedField,
2464
+ options: localizedField.options.map((option) => {
2465
+ const translations = removeLocaleRecord(option.translations, localeToRemove);
2466
+ const translationMetadata = removeLocaleRecord(option.translationMetadata, localeToRemove);
2467
+ const { translations: _translations, translationMetadata: _translationMetadata, ...base } = option;
2468
+ return {
2469
+ ...base,
2470
+ ...translations === void 0 ? {} : { translations },
2471
+ ...translationMetadata === void 0 ? {} : { translationMetadata }
2472
+ };
2473
+ })
2474
+ };
2475
+ });
2476
+ const pages = schema.pages?.map((page) => removeLocalizedNodeLocale(page, localeToRemove));
2477
+ return {
2478
+ ...form,
2479
+ supportedLocales: (schema.supportedLocales ?? []).filter((locale) => locale !== localeToRemove),
2480
+ fields,
2481
+ ...pages === void 0 ? {} : { pages }
2482
+ };
2483
+ };
2336
2484
  function collectTranslationSlots(schema, locale) {
2337
2485
  return translationSlots(schema, locale).map((descriptor) => descriptor.slot);
2338
2486
  }
@@ -2396,7 +2544,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2396
2544
  const skippedReasons = {};
2397
2545
  let result = schema;
2398
2546
  for (const locale of locales) {
2399
- const descriptors = translationSlots(schema, locale);
2547
+ const descriptors = translationSlots(schema, locale, options);
2400
2548
  if (options.markStaleTranslations ?? true) {
2401
2549
  for (const descriptor of descriptors) {
2402
2550
  if (descriptor.slot.status === "stale" || descriptor.slot.status === "manual-stale")
@@ -2406,8 +2554,9 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2406
2554
  const selected = [];
2407
2555
  for (const descriptor of descriptors) {
2408
2556
  const status = descriptor.slot.status ?? "missing";
2409
- const manual = status === "manual" || status === "manual-stale";
2410
- const shouldTranslate = options.shouldOverwrite?.(descriptor.slot) ?? ((!(options.preserveManualTranslations ?? true) || !manual) && (options.overwrite === "all" || (options.overwrite === "stale-and-missing" || options.overwrite === void 0 ? status === "missing" || status === "stale" || !(options.preserveManualTranslations ?? true) && status === "manual-stale" : status === "missing")));
2557
+ const manual = descriptor.manual || status === "manual" || status === "manual-stale";
2558
+ const requestedOverwrite = options.shouldOverwrite?.(descriptor.slot);
2559
+ const shouldTranslate = (options.preserveManualTranslations ?? true) && manual ? false : requestedOverwrite ?? (options.overwrite === "all" || (options.overwrite === "stale-and-missing" || options.overwrite === void 0 ? status === "missing" || status === "stale" || !(options.preserveManualTranslations ?? true) && status === "manual-stale" : status === "missing"));
2411
2560
  if (shouldTranslate) selected.push(descriptor);
2412
2561
  else {
2413
2562
  skippedSlots.push(descriptor.slot);
@@ -2768,15 +2917,18 @@ export {
2768
2917
  getTranslationStatus,
2769
2918
  isDisplayConditionGroupSatisfied,
2770
2919
  isDisplayConditionSatisfied,
2920
+ isManualTranslationMetadata,
2771
2921
  isQuestionVisible,
2772
2922
  iterateSubmissionPages,
2773
2923
  jsonValuesEqual,
2774
2924
  matchesSubmissionFilter,
2775
2925
  matchesSubmissionPageFilters,
2926
+ migrateSchemaTranslationMetadata,
2776
2927
  normalizeSubmissionPageSize,
2777
2928
  pipeResponsesToCsvStream,
2778
2929
  populateSchemaTranslations,
2779
2930
  publishDraft,
2931
+ removeLocaleFromSchema,
2780
2932
  resolveFormTranslation,
2781
2933
  resolveLocalizedSchema,
2782
2934
  sanitizeSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/core",
3
- "version": "4.5.0",
3
+ "version": "4.6.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },