@form-engine-ts/core 4.5.0 → 4.7.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,15 @@ 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 (directly or as `{ migrator }`) when legacy fields need
67
+ application-specific conversion. The migrator receives `TranslationMigrationContext` with the locale, JSON path,
68
+ property, node kind, and node identifiers.
69
+ `PopulateTranslationsOptions` is a compatibility alias for `PopulateTranslationOptions`, which also accepts custom
70
+ manual-translation detection and metadata normalization callbacks. `removeLocaleFromSchema(schema, locale)` removes
71
+ the locale from registrations and all form, page, field, option translation values and metadata; the default locale
72
+ cannot be removed.
73
+
65
74
  The React builder uses canonical keys with legacy aliases when resolving UI translations:
66
75
 
67
76
  | 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,165 @@ 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 defaultTranslationMetadataMigrator(metadata, sourceText, defaultLocale) {
2460
+ const record = metadata !== null && typeof metadata === "object" ? metadata : void 0;
2461
+ const sourceLocale = typeof record?.sourceLocale === "string" ? record.sourceLocale : defaultLocale;
2462
+ const translationSource = isManualTranslationMetadata(record) ? "manual" : "automatic";
2463
+ return {
2464
+ sourceLocale,
2465
+ sourceTextHash: computeSourceTextHash(sourceText),
2466
+ translationSource,
2467
+ ...typeof record?.translatedAt === "string" ? { translatedAt: record.translatedAt } : {},
2468
+ ...typeof record?.editedAt === "string" ? { editedAt: record.editedAt } : {}
2469
+ };
2470
+ }
2471
+ function migrateMetadata(metadata, sourceText, context, migrator) {
2472
+ return migrator(metadata, sourceText, context);
2473
+ }
2474
+ function isTranslationMetadataProperty(property) {
2475
+ return property === "title" || property === "description" || property === "label" || property === "completionMessage";
2476
+ }
2477
+ function migrateNodeMetadata(node, sourceTexts, contextFor, migrator) {
2478
+ if (node.translationMetadata === void 0) return node;
2479
+ const translationMetadata = Object.fromEntries(
2480
+ Object.entries(node.translationMetadata).map(([locale, properties]) => [
2481
+ locale,
2482
+ Object.fromEntries(
2483
+ Object.entries(properties).map(([property, metadata]) => {
2484
+ if (!isTranslationMetadataProperty(property)) return [property, metadata];
2485
+ return [
2486
+ property,
2487
+ migrateMetadata(metadata, sourceTexts[property] ?? "", contextFor(locale, property), migrator)
2488
+ ];
2489
+ })
2490
+ )
2491
+ ])
2492
+ );
2493
+ return { ...node, translationMetadata };
2494
+ }
2495
+ var migrateSchemaTranslationMetadata = (schema, migratorOrOptions) => {
2496
+ const defaultLocale = schema.defaultLocale ?? "";
2497
+ const migrator = typeof migratorOrOptions === "function" ? migratorOrOptions : migratorOrOptions?.migrator ?? ((metadata, sourceText, context) => defaultTranslationMetadataMigrator(metadata, sourceText, context.defaultLocale));
2498
+ const migratedSchema = migrateNodeMetadata(
2499
+ schema,
2500
+ {
2501
+ title: schema.title,
2502
+ ...schema.description === void 0 ? {} : { description: schema.description },
2503
+ ...schema.completionMessage === void 0 ? {} : { completionMessage: schema.completionMessage }
2504
+ },
2505
+ (locale, property) => ({
2506
+ locale,
2507
+ defaultLocale,
2508
+ path: property,
2509
+ property,
2510
+ nodeKind: "form"
2511
+ }),
2512
+ migrator
2513
+ );
2514
+ const fields = schema.fields.map((field) => {
2515
+ const migratedField = migrateNodeMetadata(
2516
+ field,
2517
+ { title: field.title, ...field.description === void 0 ? {} : { description: field.description } },
2518
+ (locale, property) => ({
2519
+ locale,
2520
+ defaultLocale,
2521
+ path: `fields.${field.id}.${property}`,
2522
+ property,
2523
+ nodeKind: "field",
2524
+ nodeId: field.id
2525
+ }),
2526
+ migrator
2527
+ );
2528
+ if (!("options" in migratedField)) return migratedField;
2529
+ return {
2530
+ ...migratedField,
2531
+ options: migratedField.options.map(
2532
+ (option) => migrateNodeMetadata(
2533
+ option,
2534
+ { label: option.label },
2535
+ (locale, property) => ({
2536
+ locale,
2537
+ defaultLocale,
2538
+ path: `fields.${field.id}.options.${option.id}.${property}`,
2539
+ property,
2540
+ nodeKind: "option",
2541
+ nodeId: option.id,
2542
+ parentId: field.id
2543
+ }),
2544
+ migrator
2545
+ )
2546
+ )
2547
+ };
2548
+ });
2549
+ const pages = schema.pages?.map(
2550
+ (page) => migrateNodeMetadata(
2551
+ page,
2552
+ {
2553
+ ...page.title === void 0 ? {} : { title: page.title },
2554
+ ...page.description === void 0 ? {} : { description: page.description }
2555
+ },
2556
+ (locale, property) => ({
2557
+ locale,
2558
+ defaultLocale,
2559
+ path: `pages.${page.id}.${property}`,
2560
+ property,
2561
+ nodeKind: "page",
2562
+ nodeId: page.id
2563
+ }),
2564
+ migrator
2565
+ )
2566
+ );
2567
+ return {
2568
+ ...migratedSchema,
2569
+ fields,
2570
+ ...pages === void 0 ? {} : { pages }
2571
+ };
2572
+ };
2573
+ var removeLocaleFromSchema = (schema, localeToRemove) => {
2574
+ if (localeToRemove === schema.defaultLocale) {
2575
+ throw new Error(`Cannot remove defaultLocale: ${localeToRemove}`);
2576
+ }
2577
+ const form = removeLocalizedNodeLocale(schema, localeToRemove);
2578
+ const fields = schema.fields.map((field) => {
2579
+ const localizedField = removeLocalizedNodeLocale(field, localeToRemove);
2580
+ if (!("options" in localizedField)) return localizedField;
2581
+ return {
2582
+ ...localizedField,
2583
+ options: localizedField.options.map((option) => {
2584
+ const translations = removeLocaleRecord(option.translations, localeToRemove);
2585
+ const translationMetadata = removeLocaleRecord(option.translationMetadata, localeToRemove);
2586
+ const { translations: _translations, translationMetadata: _translationMetadata, ...base } = option;
2587
+ return {
2588
+ ...base,
2589
+ ...translations === void 0 ? {} : { translations },
2590
+ ...translationMetadata === void 0 ? {} : { translationMetadata }
2591
+ };
2592
+ })
2593
+ };
2594
+ });
2595
+ const pages = schema.pages?.map((page) => removeLocalizedNodeLocale(page, localeToRemove));
2596
+ return {
2597
+ ...form,
2598
+ supportedLocales: (schema.supportedLocales ?? []).filter((locale) => locale !== localeToRemove),
2599
+ fields,
2600
+ ...pages === void 0 ? {} : { pages }
2601
+ };
2602
+ };
2409
2603
  function collectTranslationSlots(schema, locale) {
2410
2604
  return translationSlots(schema, locale).map((descriptor) => descriptor.slot);
2411
2605
  }
@@ -2469,7 +2663,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2469
2663
  const skippedReasons = {};
2470
2664
  let result = schema;
2471
2665
  for (const locale of locales) {
2472
- const descriptors = translationSlots(schema, locale);
2666
+ const descriptors = translationSlots(schema, locale, options);
2473
2667
  if (options.markStaleTranslations ?? true) {
2474
2668
  for (const descriptor of descriptors) {
2475
2669
  if (descriptor.slot.status === "stale" || descriptor.slot.status === "manual-stale")
@@ -2479,8 +2673,9 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2479
2673
  const selected = [];
2480
2674
  for (const descriptor of descriptors) {
2481
2675
  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")));
2676
+ const manual = descriptor.manual || status === "manual" || status === "manual-stale";
2677
+ const requestedOverwrite = options.shouldOverwrite?.(descriptor.slot);
2678
+ 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
2679
  if (shouldTranslate) selected.push(descriptor);
2485
2680
  else {
2486
2681
  skippedSlots.push(descriptor.slot);
@@ -2842,15 +3037,18 @@ function assertVersionMutable(status) {
2842
3037
  getTranslationStatus,
2843
3038
  isDisplayConditionGroupSatisfied,
2844
3039
  isDisplayConditionSatisfied,
3040
+ isManualTranslationMetadata,
2845
3041
  isQuestionVisible,
2846
3042
  iterateSubmissionPages,
2847
3043
  jsonValuesEqual,
2848
3044
  matchesSubmissionFilter,
2849
3045
  matchesSubmissionPageFilters,
3046
+ migrateSchemaTranslationMetadata,
2850
3047
  normalizeSubmissionPageSize,
2851
3048
  pipeResponsesToCsvStream,
2852
3049
  populateSchemaTranslations,
2853
3050
  publishDraft,
3051
+ removeLocaleFromSchema,
2854
3052
  resolveFormTranslation,
2855
3053
  resolveLocalizedSchema,
2856
3054
  sanitizeSchema,
package/dist/index.d.cts CHANGED
@@ -686,15 +686,55 @@ 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
+ interface TranslationMigrationContext {
701
+ /** Target locale code, for example "en" or "zh-Hans". */
702
+ readonly locale: string;
703
+ /** The schema's default locale. */
704
+ readonly defaultLocale: string;
705
+ /** JSON path of the translated property. */
706
+ readonly path: string;
707
+ /** Translated property name. */
708
+ readonly property: "title" | "description" | "label" | "completionMessage";
709
+ /** Kind of node that owns the translated property. */
710
+ readonly nodeKind: "form" | "page" | "field" | "option";
711
+ /** Identifier of the owning node. */
712
+ readonly nodeId?: string;
713
+ /** Identifier of the parent node, used for options. */
714
+ readonly parentId?: string;
715
+ }
716
+ type TranslationMetadataMigrator = (oldMeta: unknown, sourceText: string, context: TranslationMigrationContext) => CanonicalTranslationMetadata;
717
+ interface MigrateSchemaTranslationMetadataOptions {
718
+ /** Custom migration function used instead of the built-in legacy normalizer. */
719
+ readonly migrator?: TranslationMetadataMigrator;
720
+ }
721
+ declare const isManualTranslationMetadata: (metadata?: LegacyTranslationMetadata | CanonicalTranslationMetadata) => boolean;
689
722
  interface PopulateTranslationOptions {
690
723
  readonly overwrite?: "all" | "missing-only" | "stale-and-missing";
691
724
  readonly preserveManualTranslations?: boolean;
692
725
  readonly markStaleTranslations?: boolean;
693
726
  readonly shouldOverwrite?: (slot: TranslationSlot) => boolean;
694
727
  readonly createMetadata?: (slot: TranslationSlot, translatedText: string) => Readonly<Record<string, JsonValue>>;
728
+ readonly isManualTranslation?: (metadata: unknown, context: {
729
+ readonly path: string;
730
+ readonly locale: string;
731
+ }) => boolean;
732
+ readonly normalizeMetadata?: (metadata: unknown, sourceText: string) => CanonicalTranslationMetadata;
695
733
  /** Applies locale admission and count limits before the adapter is called. */
696
734
  readonly policy?: Pick<FormPolicy, "allowedLocales" | "maxLocales">;
697
735
  }
736
+ /** Compatibility alias for clients that used the pluralized options name. */
737
+ type PopulateTranslationsOptions = PopulateTranslationOptions;
698
738
  interface TranslationReport {
699
739
  readonly updatedSlots: readonly TranslationSlot[];
700
740
  readonly skippedSlots: readonly TranslationSlot[];
@@ -703,6 +743,9 @@ interface TranslationReport {
703
743
  }
704
744
  declare const computeSourceTextHash: (text: string) => string;
705
745
  declare function getTranslationStatus(sourceText: string, translatedText: string | undefined, metadata: CanonicalTranslationMetadata | Readonly<Record<string, JsonValue>> | undefined): TranslationStatus;
746
+ declare const migrateSchemaTranslationMetadata: (schema: FormSchema, migratorOrOptions?: ((oldMeta: unknown, sourceText: string) => CanonicalTranslationMetadata) | TranslationMetadataMigrator | MigrateSchemaTranslationMetadataOptions) => FormSchema;
747
+ /** Removes a locale registration and every localized value and metadata entry for it. */
748
+ declare const removeLocaleFromSchema: (schema: FormSchema, localeToRemove: string) => FormSchema;
706
749
  declare function collectTranslationSlots(schema: FormSchema, locale: string): readonly TranslationSlot[];
707
750
  declare function resolveLocalizedSchema(schema: FormSchema, targetLocale?: string): FormSchema;
708
751
  declare function populateSchemaTranslations(schema: FormSchema, targetLocales: readonly string[], adapter: AsyncTranslationAdapter, options?: PopulateTranslationOptions): Promise<{
@@ -725,4 +768,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
725
768
  declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
726
769
  declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
727
770
 
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 };
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 };
package/dist/index.d.ts CHANGED
@@ -686,15 +686,55 @@ 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
+ interface TranslationMigrationContext {
701
+ /** Target locale code, for example "en" or "zh-Hans". */
702
+ readonly locale: string;
703
+ /** The schema's default locale. */
704
+ readonly defaultLocale: string;
705
+ /** JSON path of the translated property. */
706
+ readonly path: string;
707
+ /** Translated property name. */
708
+ readonly property: "title" | "description" | "label" | "completionMessage";
709
+ /** Kind of node that owns the translated property. */
710
+ readonly nodeKind: "form" | "page" | "field" | "option";
711
+ /** Identifier of the owning node. */
712
+ readonly nodeId?: string;
713
+ /** Identifier of the parent node, used for options. */
714
+ readonly parentId?: string;
715
+ }
716
+ type TranslationMetadataMigrator = (oldMeta: unknown, sourceText: string, context: TranslationMigrationContext) => CanonicalTranslationMetadata;
717
+ interface MigrateSchemaTranslationMetadataOptions {
718
+ /** Custom migration function used instead of the built-in legacy normalizer. */
719
+ readonly migrator?: TranslationMetadataMigrator;
720
+ }
721
+ declare const isManualTranslationMetadata: (metadata?: LegacyTranslationMetadata | CanonicalTranslationMetadata) => boolean;
689
722
  interface PopulateTranslationOptions {
690
723
  readonly overwrite?: "all" | "missing-only" | "stale-and-missing";
691
724
  readonly preserveManualTranslations?: boolean;
692
725
  readonly markStaleTranslations?: boolean;
693
726
  readonly shouldOverwrite?: (slot: TranslationSlot) => boolean;
694
727
  readonly createMetadata?: (slot: TranslationSlot, translatedText: string) => Readonly<Record<string, JsonValue>>;
728
+ readonly isManualTranslation?: (metadata: unknown, context: {
729
+ readonly path: string;
730
+ readonly locale: string;
731
+ }) => boolean;
732
+ readonly normalizeMetadata?: (metadata: unknown, sourceText: string) => CanonicalTranslationMetadata;
695
733
  /** Applies locale admission and count limits before the adapter is called. */
696
734
  readonly policy?: Pick<FormPolicy, "allowedLocales" | "maxLocales">;
697
735
  }
736
+ /** Compatibility alias for clients that used the pluralized options name. */
737
+ type PopulateTranslationsOptions = PopulateTranslationOptions;
698
738
  interface TranslationReport {
699
739
  readonly updatedSlots: readonly TranslationSlot[];
700
740
  readonly skippedSlots: readonly TranslationSlot[];
@@ -703,6 +743,9 @@ interface TranslationReport {
703
743
  }
704
744
  declare const computeSourceTextHash: (text: string) => string;
705
745
  declare function getTranslationStatus(sourceText: string, translatedText: string | undefined, metadata: CanonicalTranslationMetadata | Readonly<Record<string, JsonValue>> | undefined): TranslationStatus;
746
+ declare const migrateSchemaTranslationMetadata: (schema: FormSchema, migratorOrOptions?: ((oldMeta: unknown, sourceText: string) => CanonicalTranslationMetadata) | TranslationMetadataMigrator | MigrateSchemaTranslationMetadataOptions) => FormSchema;
747
+ /** Removes a locale registration and every localized value and metadata entry for it. */
748
+ declare const removeLocaleFromSchema: (schema: FormSchema, localeToRemove: string) => FormSchema;
706
749
  declare function collectTranslationSlots(schema: FormSchema, locale: string): readonly TranslationSlot[];
707
750
  declare function resolveLocalizedSchema(schema: FormSchema, targetLocale?: string): FormSchema;
708
751
  declare function populateSchemaTranslations(schema: FormSchema, targetLocales: readonly string[], adapter: AsyncTranslationAdapter, options?: PopulateTranslationOptions): Promise<{
@@ -725,4 +768,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
725
768
  declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
726
769
  declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
727
770
 
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 };
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 };
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,165 @@ 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 defaultTranslationMetadataMigrator(metadata, sourceText, defaultLocale) {
2384
+ const record = metadata !== null && typeof metadata === "object" ? metadata : void 0;
2385
+ const sourceLocale = typeof record?.sourceLocale === "string" ? record.sourceLocale : defaultLocale;
2386
+ const translationSource = isManualTranslationMetadata(record) ? "manual" : "automatic";
2387
+ return {
2388
+ sourceLocale,
2389
+ sourceTextHash: computeSourceTextHash(sourceText),
2390
+ translationSource,
2391
+ ...typeof record?.translatedAt === "string" ? { translatedAt: record.translatedAt } : {},
2392
+ ...typeof record?.editedAt === "string" ? { editedAt: record.editedAt } : {}
2393
+ };
2394
+ }
2395
+ function migrateMetadata(metadata, sourceText, context, migrator) {
2396
+ return migrator(metadata, sourceText, context);
2397
+ }
2398
+ function isTranslationMetadataProperty(property) {
2399
+ return property === "title" || property === "description" || property === "label" || property === "completionMessage";
2400
+ }
2401
+ function migrateNodeMetadata(node, sourceTexts, contextFor, migrator) {
2402
+ if (node.translationMetadata === void 0) return node;
2403
+ const translationMetadata = Object.fromEntries(
2404
+ Object.entries(node.translationMetadata).map(([locale, properties]) => [
2405
+ locale,
2406
+ Object.fromEntries(
2407
+ Object.entries(properties).map(([property, metadata]) => {
2408
+ if (!isTranslationMetadataProperty(property)) return [property, metadata];
2409
+ return [
2410
+ property,
2411
+ migrateMetadata(metadata, sourceTexts[property] ?? "", contextFor(locale, property), migrator)
2412
+ ];
2413
+ })
2414
+ )
2415
+ ])
2416
+ );
2417
+ return { ...node, translationMetadata };
2418
+ }
2419
+ var migrateSchemaTranslationMetadata = (schema, migratorOrOptions) => {
2420
+ const defaultLocale = schema.defaultLocale ?? "";
2421
+ const migrator = typeof migratorOrOptions === "function" ? migratorOrOptions : migratorOrOptions?.migrator ?? ((metadata, sourceText, context) => defaultTranslationMetadataMigrator(metadata, sourceText, context.defaultLocale));
2422
+ const migratedSchema = migrateNodeMetadata(
2423
+ schema,
2424
+ {
2425
+ title: schema.title,
2426
+ ...schema.description === void 0 ? {} : { description: schema.description },
2427
+ ...schema.completionMessage === void 0 ? {} : { completionMessage: schema.completionMessage }
2428
+ },
2429
+ (locale, property) => ({
2430
+ locale,
2431
+ defaultLocale,
2432
+ path: property,
2433
+ property,
2434
+ nodeKind: "form"
2435
+ }),
2436
+ migrator
2437
+ );
2438
+ const fields = schema.fields.map((field) => {
2439
+ const migratedField = migrateNodeMetadata(
2440
+ field,
2441
+ { title: field.title, ...field.description === void 0 ? {} : { description: field.description } },
2442
+ (locale, property) => ({
2443
+ locale,
2444
+ defaultLocale,
2445
+ path: `fields.${field.id}.${property}`,
2446
+ property,
2447
+ nodeKind: "field",
2448
+ nodeId: field.id
2449
+ }),
2450
+ migrator
2451
+ );
2452
+ if (!("options" in migratedField)) return migratedField;
2453
+ return {
2454
+ ...migratedField,
2455
+ options: migratedField.options.map(
2456
+ (option) => migrateNodeMetadata(
2457
+ option,
2458
+ { label: option.label },
2459
+ (locale, property) => ({
2460
+ locale,
2461
+ defaultLocale,
2462
+ path: `fields.${field.id}.options.${option.id}.${property}`,
2463
+ property,
2464
+ nodeKind: "option",
2465
+ nodeId: option.id,
2466
+ parentId: field.id
2467
+ }),
2468
+ migrator
2469
+ )
2470
+ )
2471
+ };
2472
+ });
2473
+ const pages = schema.pages?.map(
2474
+ (page) => migrateNodeMetadata(
2475
+ page,
2476
+ {
2477
+ ...page.title === void 0 ? {} : { title: page.title },
2478
+ ...page.description === void 0 ? {} : { description: page.description }
2479
+ },
2480
+ (locale, property) => ({
2481
+ locale,
2482
+ defaultLocale,
2483
+ path: `pages.${page.id}.${property}`,
2484
+ property,
2485
+ nodeKind: "page",
2486
+ nodeId: page.id
2487
+ }),
2488
+ migrator
2489
+ )
2490
+ );
2491
+ return {
2492
+ ...migratedSchema,
2493
+ fields,
2494
+ ...pages === void 0 ? {} : { pages }
2495
+ };
2496
+ };
2497
+ var removeLocaleFromSchema = (schema, localeToRemove) => {
2498
+ if (localeToRemove === schema.defaultLocale) {
2499
+ throw new Error(`Cannot remove defaultLocale: ${localeToRemove}`);
2500
+ }
2501
+ const form = removeLocalizedNodeLocale(schema, localeToRemove);
2502
+ const fields = schema.fields.map((field) => {
2503
+ const localizedField = removeLocalizedNodeLocale(field, localeToRemove);
2504
+ if (!("options" in localizedField)) return localizedField;
2505
+ return {
2506
+ ...localizedField,
2507
+ options: localizedField.options.map((option) => {
2508
+ const translations = removeLocaleRecord(option.translations, localeToRemove);
2509
+ const translationMetadata = removeLocaleRecord(option.translationMetadata, localeToRemove);
2510
+ const { translations: _translations, translationMetadata: _translationMetadata, ...base } = option;
2511
+ return {
2512
+ ...base,
2513
+ ...translations === void 0 ? {} : { translations },
2514
+ ...translationMetadata === void 0 ? {} : { translationMetadata }
2515
+ };
2516
+ })
2517
+ };
2518
+ });
2519
+ const pages = schema.pages?.map((page) => removeLocalizedNodeLocale(page, localeToRemove));
2520
+ return {
2521
+ ...form,
2522
+ supportedLocales: (schema.supportedLocales ?? []).filter((locale) => locale !== localeToRemove),
2523
+ fields,
2524
+ ...pages === void 0 ? {} : { pages }
2525
+ };
2526
+ };
2336
2527
  function collectTranslationSlots(schema, locale) {
2337
2528
  return translationSlots(schema, locale).map((descriptor) => descriptor.slot);
2338
2529
  }
@@ -2396,7 +2587,7 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2396
2587
  const skippedReasons = {};
2397
2588
  let result = schema;
2398
2589
  for (const locale of locales) {
2399
- const descriptors = translationSlots(schema, locale);
2590
+ const descriptors = translationSlots(schema, locale, options);
2400
2591
  if (options.markStaleTranslations ?? true) {
2401
2592
  for (const descriptor of descriptors) {
2402
2593
  if (descriptor.slot.status === "stale" || descriptor.slot.status === "manual-stale")
@@ -2406,8 +2597,9 @@ async function populateSchemaTranslations(schema, targetLocales, adapter, option
2406
2597
  const selected = [];
2407
2598
  for (const descriptor of descriptors) {
2408
2599
  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")));
2600
+ const manual = descriptor.manual || status === "manual" || status === "manual-stale";
2601
+ const requestedOverwrite = options.shouldOverwrite?.(descriptor.slot);
2602
+ 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
2603
  if (shouldTranslate) selected.push(descriptor);
2412
2604
  else {
2413
2605
  skippedSlots.push(descriptor.slot);
@@ -2768,15 +2960,18 @@ export {
2768
2960
  getTranslationStatus,
2769
2961
  isDisplayConditionGroupSatisfied,
2770
2962
  isDisplayConditionSatisfied,
2963
+ isManualTranslationMetadata,
2771
2964
  isQuestionVisible,
2772
2965
  iterateSubmissionPages,
2773
2966
  jsonValuesEqual,
2774
2967
  matchesSubmissionFilter,
2775
2968
  matchesSubmissionPageFilters,
2969
+ migrateSchemaTranslationMetadata,
2776
2970
  normalizeSubmissionPageSize,
2777
2971
  pipeResponsesToCsvStream,
2778
2972
  populateSchemaTranslations,
2779
2973
  publishDraft,
2974
+ removeLocaleFromSchema,
2780
2975
  resolveFormTranslation,
2781
2976
  resolveLocalizedSchema,
2782
2977
  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.7.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },