@verbatra/sdk 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -709,7 +709,7 @@ interface DiffDeps {
709
709
  declare function diff(input: DiffInput, deps?: DiffDeps): Promise<DiffSummary>;
710
710
 
711
711
  /** A stable code for an SDK-originated notice (not a provider notice). */
712
- type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED";
712
+ type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED";
713
713
  /**
714
714
  * A notice raised by the SDK itself (not a provider), structurally identical to a {@link ProviderNotice}
715
715
  * so both share the {@link LocaleSummary.notices} channel. Carries only a stable code and a static,
@@ -746,8 +746,20 @@ interface LocaleSummary {
746
746
  readonly pruned: readonly string[];
747
747
  /** Source keys flagged invalid-ICU that were skipped for translation this run. */
748
748
  readonly invalidIcuSource: readonly string[];
749
- /** Translated keys that failed the placeholder-integrity check and were withheld. */
749
+ /**
750
+ * Translated keys that failed the placeholder-integrity check and were withheld. Never includes a
751
+ * key withheld because the provider call itself failed; see {@link LocaleSummary.providerFailures}
752
+ * for that case.
753
+ */
750
754
  readonly integrityMismatches: readonly string[];
755
+ /**
756
+ * Keys withheld because the provider call itself failed (for example a revoked API key, a rate
757
+ * limit, or a network timeout), as distinct from a translation that came back and failed the
758
+ * placeholder-integrity check. Nothing was translated for these keys. The corresponding
759
+ * {@link LocaleSummary.notices} entry carries the secret-free failure code and message. Empty in a
760
+ * dry-run and empty unless a provider call threw.
761
+ */
762
+ readonly providerFailures: readonly string[];
751
763
  /**
752
764
  * Plural-category keys verbatra synthesized this run (for example a Polish `items_few` the source
753
765
  * never supplied), kept distinct from {@link translated}. Empty unless plural generation was enabled
package/dist/index.d.ts CHANGED
@@ -709,7 +709,7 @@ interface DiffDeps {
709
709
  declare function diff(input: DiffInput, deps?: DiffDeps): Promise<DiffSummary>;
710
710
 
711
711
  /** A stable code for an SDK-originated notice (not a provider notice). */
712
- type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED";
712
+ type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED";
713
713
  /**
714
714
  * A notice raised by the SDK itself (not a provider), structurally identical to a {@link ProviderNotice}
715
715
  * so both share the {@link LocaleSummary.notices} channel. Carries only a stable code and a static,
@@ -746,8 +746,20 @@ interface LocaleSummary {
746
746
  readonly pruned: readonly string[];
747
747
  /** Source keys flagged invalid-ICU that were skipped for translation this run. */
748
748
  readonly invalidIcuSource: readonly string[];
749
- /** Translated keys that failed the placeholder-integrity check and were withheld. */
749
+ /**
750
+ * Translated keys that failed the placeholder-integrity check and were withheld. Never includes a
751
+ * key withheld because the provider call itself failed; see {@link LocaleSummary.providerFailures}
752
+ * for that case.
753
+ */
750
754
  readonly integrityMismatches: readonly string[];
755
+ /**
756
+ * Keys withheld because the provider call itself failed (for example a revoked API key, a rate
757
+ * limit, or a network timeout), as distinct from a translation that came back and failed the
758
+ * placeholder-integrity check. Nothing was translated for these keys. The corresponding
759
+ * {@link LocaleSummary.notices} entry carries the secret-free failure code and message. Empty in a
760
+ * dry-run and empty unless a provider call threw.
761
+ */
762
+ readonly providerFailures: readonly string[];
751
763
  /**
752
764
  * Plural-category keys verbatra synthesized this run (for example a Polish `items_few` the source
753
765
  * never supplied), kept distinct from {@link translated}. Empty unless plural generation was enabled
package/dist/index.js CHANGED
@@ -1152,19 +1152,53 @@ function childMessages(element) {
1152
1152
  }
1153
1153
  return [];
1154
1154
  }
1155
- function collect(elements, add, state) {
1155
+ function addCount(target, token, count) {
1156
+ target.set(token, (target.get(token) ?? 0) + count);
1157
+ }
1158
+ function combineBranches(branchMultisets) {
1159
+ const combined = /* @__PURE__ */ new Map();
1160
+ const tokens = /* @__PURE__ */ new Set();
1161
+ for (const branch of branchMultisets) {
1162
+ for (const token of branch.keys()) {
1163
+ tokens.add(token);
1164
+ }
1165
+ }
1166
+ for (const token of tokens) {
1167
+ const minCount = Math.min(...branchMultisets.map((branch) => branch.get(token) ?? 0));
1168
+ if (minCount > 0) {
1169
+ combined.set(token, minCount);
1170
+ }
1171
+ }
1172
+ return combined;
1173
+ }
1174
+ function collect(elements, state) {
1175
+ const result = /* @__PURE__ */ new Map();
1156
1176
  for (const element of elements) {
1157
1177
  const token = tokenOf(element);
1158
1178
  if (token !== void 0) {
1159
- add(token);
1179
+ addCount(result, token, 1);
1160
1180
  }
1161
1181
  if (element.type === TYPE.plural) {
1162
1182
  state.isPlural = true;
1163
1183
  }
1164
- for (const child of childMessages(element)) {
1165
- collect(child, add, state);
1184
+ const branches = childMessages(element);
1185
+ if (branches.length > 0) {
1186
+ const branchMultisets = branches.map((branch) => collect(branch, state));
1187
+ for (const [branchToken, branchCount] of combineBranches(branchMultisets)) {
1188
+ addCount(result, branchToken, branchCount);
1189
+ }
1190
+ }
1191
+ }
1192
+ return result;
1193
+ }
1194
+ function flatten(multiset) {
1195
+ const flattened = [];
1196
+ for (const [token, count] of multiset) {
1197
+ for (let i = 0; i < count; i += 1) {
1198
+ flattened.push(token);
1166
1199
  }
1167
1200
  }
1201
+ return flattened;
1168
1202
  }
1169
1203
  function analyzeIcuValue(value) {
1170
1204
  if (!value.includes("{") && !value.includes("<")) {
@@ -1172,16 +1206,9 @@ function analyzeIcuValue(value) {
1172
1206
  }
1173
1207
  try {
1174
1208
  const ast = parse$1(value);
1175
- const placeholders = [];
1176
1209
  const state = { isPlural: false };
1177
- collect(
1178
- ast,
1179
- (token) => {
1180
- placeholders.push(token);
1181
- },
1182
- state
1183
- );
1184
- return { placeholders, isPlural: state.isPlural, valid: true };
1210
+ const multiset = collect(ast, state);
1211
+ return { placeholders: flatten(multiset), isPlural: state.isPlural, valid: true };
1185
1212
  } catch {
1186
1213
  return INVALID;
1187
1214
  }
@@ -1443,21 +1470,28 @@ function addEntries(ctx, prefix, node) {
1443
1470
  }
1444
1471
  }
1445
1472
  }
1446
- function addPathEntries(node, prefix, namespace, derive, out) {
1473
+ function addPathEntries(node, prefix, namespace, derive, out, claimedPaths) {
1447
1474
  for (const [key, value] of Object.entries(node)) {
1448
1475
  const path = prefix === "" ? key : `${prefix}.${key}`;
1476
+ if (claimedPaths.has(path)) {
1477
+ throw new AdapterError(
1478
+ "INVALID_STRUCTURE",
1479
+ "A dotted key and a nested key path resolve to the same path."
1480
+ );
1481
+ }
1482
+ claimedPaths.add(path);
1449
1483
  if (typeof value === "string") {
1450
1484
  const { placeholders, isPlural } = derive(key, value);
1451
1485
  out.set(path, { key: path, namespace, value, placeholders, isPlural });
1452
1486
  } else {
1453
- addPathEntries(value, path, namespace, derive, out);
1487
+ addPathEntries(value, path, namespace, derive, out, claimedPaths);
1454
1488
  }
1455
1489
  }
1456
1490
  }
1457
1491
  function flattenTree(tree, namespace, derive, keyMode = "literal-leaf") {
1458
1492
  const out = /* @__PURE__ */ new Map();
1459
1493
  if (keyMode === "path-notation") {
1460
- addPathEntries(tree, "", namespace, derive, out);
1494
+ addPathEntries(tree, "", namespace, derive, out, /* @__PURE__ */ new Set());
1461
1495
  return out;
1462
1496
  }
1463
1497
  addEntries({ namespace, derive, out, claimed: /* @__PURE__ */ new Map() }, [], tree);
@@ -1579,21 +1613,20 @@ function messagesFromEntries(entries) {
1579
1613
  }
1580
1614
  return out;
1581
1615
  }
1616
+ function isEnoent(error) {
1617
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1618
+ }
1582
1619
  async function readDestinationPairs(filePath) {
1583
- let parsed;
1620
+ let content;
1584
1621
  try {
1585
- const outcome = await readBounded2(filePath);
1586
- if (outcome.kind !== "ok") {
1622
+ content = await readFileContent(filePath);
1623
+ } catch (error) {
1624
+ if (isEnoent(error)) {
1587
1625
  return null;
1588
1626
  }
1589
- parsed = JSON.parse(outcome.content);
1590
- } catch {
1591
- return null;
1592
- }
1593
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1594
- return null;
1627
+ throw error;
1595
1628
  }
1596
- return Object.entries(parsed);
1629
+ return Object.entries(parseArbObject(content));
1597
1630
  }
1598
1631
  async function buildArbWriteTree(entries, filePath) {
1599
1632
  const messages = messagesFromEntries(entries);
@@ -1694,6 +1727,20 @@ function createNextIntlJsonAdapter() {
1694
1727
  validateMessage: icuIsValid
1695
1728
  });
1696
1729
  }
1730
+ function assertNoDottedNestedKey(tree) {
1731
+ for (const [key, value] of Object.entries(tree)) {
1732
+ if (typeof value !== "object") {
1733
+ continue;
1734
+ }
1735
+ if (key.includes(".")) {
1736
+ throw new AdapterError(
1737
+ "MIXED_STRUCTURE",
1738
+ "A nested object key contains a literal dot, which is ambiguous with a dotted path."
1739
+ );
1740
+ }
1741
+ assertNoDottedNestedKey(value);
1742
+ }
1743
+ }
1697
1744
  function assertNotMixed(tree) {
1698
1745
  let hasNested = false;
1699
1746
  let hasFlatDottedKey = false;
@@ -1710,6 +1757,7 @@ function assertNotMixed(tree) {
1710
1757
  "The file mixes flat dotted keys with nested objects."
1711
1758
  );
1712
1759
  }
1760
+ assertNoDottedNestedKey(tree);
1713
1761
  }
1714
1762
  async function detectStyle(filePath) {
1715
1763
  let parsed;
@@ -2207,6 +2255,7 @@ function failureSummary(locale, error) {
2207
2255
  pruned: [],
2208
2256
  invalidIcuSource: [],
2209
2257
  integrityMismatches: [],
2258
+ providerFailures: [],
2210
2259
  generated: [],
2211
2260
  notices: [],
2212
2261
  error: describeError(error)
@@ -2218,6 +2267,29 @@ function partition(locales) {
2218
2267
  return { succeeded, failed };
2219
2268
  }
2220
2269
 
2270
+ // src/flow/batching.ts
2271
+ function chunk(items, size) {
2272
+ const chunks = [];
2273
+ for (let index = 0; index < items.length; index += size) {
2274
+ chunks.push(items.slice(index, index + size));
2275
+ }
2276
+ return chunks;
2277
+ }
2278
+ var GENERIC_PROVIDER_FAILURE_MESSAGE = "The provider call failed.";
2279
+ function classifyProviderFailure(error) {
2280
+ if (error instanceof ProviderError) {
2281
+ return { code: error.code, message: error.message };
2282
+ }
2283
+ return { code: "PROVIDER_CALL_FAILED", message: GENERIC_PROVIDER_FAILURE_MESSAGE };
2284
+ }
2285
+ function subBatchFailedNotice(count, error) {
2286
+ const { code, message } = classifyProviderFailure(error);
2287
+ return {
2288
+ code: "SUB_BATCH_FAILED",
2289
+ message: `A sub-batch of ${count} entries failed (${code}: ${message}) and was withheld; it will be retried next run.`
2290
+ };
2291
+ }
2292
+
2221
2293
  // src/flow/notices.ts
2222
2294
  function isNotice(value) {
2223
2295
  return typeof value === "object" && value !== null && typeof value.code === "string" && typeof value.message === "string";
@@ -2400,13 +2472,36 @@ async function generatePluralForms(context) {
2400
2472
  const plan = planPluralGeneration(context.source, context.targetLocale, context.format);
2401
2473
  const stale = staleItems(plan.items, context.baseline);
2402
2474
  if (stale.length === 0) {
2403
- return { accepted: [], withheld: [] };
2475
+ return { accepted: [], withheld: [], providerFailures: [], notices: [] };
2404
2476
  }
2405
- const entries = stale.map(syntheticEntry);
2406
- const result = await context.provider.translateBatch(buildRequest2(context, entries));
2407
2477
  const accepted = [];
2408
2478
  const withheld = [];
2409
- for (const item of stale) {
2479
+ const providerFailures = [];
2480
+ const notices = [];
2481
+ for (const batch of chunk(stale, context.maxBatchSize)) {
2482
+ const batchNotices = await runGenerationSubBatch(
2483
+ context,
2484
+ batch,
2485
+ accepted,
2486
+ withheld,
2487
+ providerFailures
2488
+ );
2489
+ notices.push(...batchNotices);
2490
+ }
2491
+ return { accepted, withheld, providerFailures, notices };
2492
+ }
2493
+ async function runGenerationSubBatch(context, batch, accepted, withheld, providerFailures) {
2494
+ let result;
2495
+ try {
2496
+ const entries = batch.map(syntheticEntry);
2497
+ result = await context.provider.translateBatch(buildRequest2(context, entries));
2498
+ } catch (error) {
2499
+ for (const item of batch) {
2500
+ providerFailures.push(item.targetKey);
2501
+ }
2502
+ return [subBatchFailedNotice(batch.length, error)];
2503
+ }
2504
+ for (const item of batch) {
2410
2505
  const value = result.values.get(item.targetKey);
2411
2506
  const integrity = result.integrity.get(item.targetKey);
2412
2507
  if (value !== void 0 && integrity?.matches === true) {
@@ -2419,7 +2514,7 @@ async function generatePluralForms(context) {
2419
2514
  withheld.push(item.targetKey);
2420
2515
  }
2421
2516
  }
2422
- return { accepted, withheld };
2517
+ return readNotices(result);
2423
2518
  }
2424
2519
 
2425
2520
  // src/flow/locale-run.ts
@@ -2469,6 +2564,7 @@ async function runLocale(params) {
2469
2564
  translated: toTranslate,
2470
2565
  generated: [],
2471
2566
  integrityMismatches: [],
2567
+ providerFailures: [],
2472
2568
  pruned,
2473
2569
  notices: sdkNotices
2474
2570
  }),
@@ -2478,12 +2574,14 @@ async function runLocale(params) {
2478
2574
  const entries = toTranslate.map((key) => params.source.entries.get(key)).filter((entry) => entry !== void 0);
2479
2575
  const accepted = /* @__PURE__ */ new Map();
2480
2576
  const integrityMismatches = [];
2577
+ const providerFailures = [];
2481
2578
  const subBatchNotices = await translateAndCheck(
2482
2579
  provider,
2483
2580
  params,
2484
2581
  entries,
2485
2582
  accepted,
2486
- integrityMismatches
2583
+ integrityMismatches,
2584
+ providerFailures
2487
2585
  );
2488
2586
  const merged = new Map(target.entries);
2489
2587
  for (const key of pruned) {
@@ -2507,8 +2605,18 @@ async function runLocale(params) {
2507
2605
  path
2508
2606
  );
2509
2607
  const pluralNotices = params.generatePlurals ? pluralNoticeFor(params, merged) : sdkNotices;
2510
- const notices = [...pluralNotices, ...subBatchNotices];
2511
- const withheld = /* @__PURE__ */ new Set([...integrityMismatches, ...invalidIcuSource, ...generation.withheld]);
2608
+ const notices = [
2609
+ ...pluralNotices,
2610
+ ...subBatchNotices,
2611
+ ...generation.notices
2612
+ ];
2613
+ const withheld = /* @__PURE__ */ new Set([
2614
+ ...integrityMismatches,
2615
+ ...providerFailures,
2616
+ ...invalidIcuSource,
2617
+ ...generation.withheld,
2618
+ ...generation.providerFailures
2619
+ ]);
2512
2620
  return {
2513
2621
  summary: baseSummary({
2514
2622
  locale: params.targetLocale,
@@ -2519,6 +2627,8 @@ async function runLocale(params) {
2519
2627
  generated: generation.accepted.map((form) => form.targetKey).sort(),
2520
2628
  // Withheld generated forms surface alongside withheld translations: both failed integrity.
2521
2629
  integrityMismatches: [...integrityMismatches, ...generation.withheld].sort(),
2630
+ // A generation sub-batch whose provider call itself threw is a provider failure, never integrity.
2631
+ providerFailures: [...providerFailures, ...generation.providerFailures].sort(),
2522
2632
  pruned,
2523
2633
  notices
2524
2634
  }),
@@ -2527,7 +2637,7 @@ async function runLocale(params) {
2527
2637
  }
2528
2638
  async function runGeneration(params, provider) {
2529
2639
  if (!params.generatePlurals || provider.kind !== "llm") {
2530
- return { accepted: [], withheld: [] };
2640
+ return { accepted: [], withheld: [], providerFailures: [], notices: [] };
2531
2641
  }
2532
2642
  return generatePluralForms({
2533
2643
  source: params.source,
@@ -2538,7 +2648,8 @@ async function runGeneration(params, provider) {
2538
2648
  provider,
2539
2649
  glossary: params.glossary,
2540
2650
  tone: params.tone,
2541
- baseline: params.baseline
2651
+ baseline: params.baseline,
2652
+ maxBatchSize: params.maxBatchSize
2542
2653
  });
2543
2654
  }
2544
2655
  function pluralNoticeFor(params, merged) {
@@ -2560,27 +2671,35 @@ function baseSummary(parts) {
2560
2671
  pruned: parts.pruned,
2561
2672
  invalidIcuSource: parts.invalidIcuSource,
2562
2673
  integrityMismatches: parts.integrityMismatches,
2674
+ providerFailures: parts.providerFailures,
2563
2675
  generated: parts.generated,
2564
2676
  notices: parts.notices
2565
2677
  };
2566
2678
  }
2567
- async function translateAndCheck(provider, params, entries, accepted, integrityMismatches) {
2679
+ async function translateAndCheck(provider, params, entries, accepted, integrityMismatches, providerFailures) {
2568
2680
  const notices = [];
2569
2681
  for (const batch of chunk(entries, params.maxBatchSize)) {
2570
- const subNotices = await runSubBatch(provider, params, batch, accepted, integrityMismatches);
2682
+ const subNotices = await runSubBatch(
2683
+ provider,
2684
+ params,
2685
+ batch,
2686
+ accepted,
2687
+ integrityMismatches,
2688
+ providerFailures
2689
+ );
2571
2690
  notices.push(...subNotices);
2572
2691
  }
2573
2692
  return notices;
2574
2693
  }
2575
- async function runSubBatch(provider, params, batch, accepted, integrityMismatches) {
2694
+ async function runSubBatch(provider, params, batch, accepted, integrityMismatches, providerFailures) {
2576
2695
  let result;
2577
2696
  try {
2578
2697
  result = await provider.translateBatch(buildRequest3(params, batch));
2579
- } catch {
2698
+ } catch (error) {
2580
2699
  for (const entry of batch) {
2581
- integrityMismatches.push(entry.key);
2700
+ providerFailures.push(entry.key);
2582
2701
  }
2583
- return [subBatchFailedNotice(batch.length)];
2702
+ return [subBatchFailedNotice(batch.length, error)];
2584
2703
  }
2585
2704
  for (const entry of batch) {
2586
2705
  const value = result.values.get(entry.key);
@@ -2593,19 +2712,6 @@ async function runSubBatch(provider, params, batch, accepted, integrityMismatche
2593
2712
  }
2594
2713
  return readNotices(result);
2595
2714
  }
2596
- function subBatchFailedNotice(count) {
2597
- return {
2598
- code: "SUB_BATCH_FAILED",
2599
- message: `A sub-batch of ${count} entries failed and was withheld; it will be retried next run.`
2600
- };
2601
- }
2602
- function chunk(items, size) {
2603
- const chunks = [];
2604
- for (let index = 0; index < items.length; index += size) {
2605
- chunks.push(items.slice(index, index + size));
2606
- }
2607
- return chunks;
2608
- }
2609
2715
  function computeLockEntries(params, merged, withheld, generated) {
2610
2716
  const lockEntries = {};
2611
2717
  const sourceBaseKeys = sourcePluralBaseKeys(params.source);
@@ -2753,6 +2859,7 @@ var COLUMN_WIDTHS = {
2753
2859
  [COLUMN.status]: 12,
2754
2860
  [COLUMN.translation]: 50
2755
2861
  };
2862
+ var TEXT_NUMBER_FORMAT = "@";
2756
2863
  function styleHeader(sheet) {
2757
2864
  const header = sheet.getRow(HEADER_ROW);
2758
2865
  HEADERS.forEach((label, index) => {
@@ -2768,6 +2875,7 @@ function applyColumnGeometry(sheet) {
2768
2875
  sheet.getColumn(Number(column)).width = width;
2769
2876
  }
2770
2877
  sheet.getColumn(COLUMN.sourceHash).hidden = true;
2878
+ sheet.getColumn(COLUMN.translation).numFmt = TEXT_NUMBER_FORMAT;
2771
2879
  sheet.views = [{ state: "frozen", ySplit: HEADER_ROW }];
2772
2880
  }
2773
2881
  function writeRow(sheet, sheetRow) {
@@ -2776,7 +2884,9 @@ function writeRow(sheet, sheetRow) {
2776
2884
  row.getCell(COLUMN.source).value = sheetRow.source;
2777
2885
  row.getCell(COLUMN.current).value = sheetRow.currentTarget;
2778
2886
  row.getCell(COLUMN.status).value = sheetRow.status;
2779
- row.getCell(COLUMN.translation).value = sheetRow.translation === "" ? null : sheetRow.translation;
2887
+ const translationCell = row.getCell(COLUMN.translation);
2888
+ translationCell.numFmt = TEXT_NUMBER_FORMAT;
2889
+ translationCell.value = sheetRow.translation === "" ? null : sheetRow.translation;
2780
2890
  row.getCell(COLUMN.sourceHash).value = sheetRow.sourceHash;
2781
2891
  for (let column = COLUMN.key; column <= COLUMN.sourceHash; column += 1) {
2782
2892
  const cell = row.getCell(column);
@@ -3128,9 +3238,20 @@ function judge(row, sourceEntry, adapter) {
3128
3238
  }
3129
3239
  return void 0;
3130
3240
  }
3241
+ function trackBlankDrift(row, params, buckets) {
3242
+ const sourceEntry = params.source.entries.get(row.key);
3243
+ if (sourceEntry === void 0) {
3244
+ return;
3245
+ }
3246
+ const priorHash = params.baseline.get(row.key);
3247
+ if (priorHash !== void 0 && priorHash !== contentHash(sourceEntry)) {
3248
+ buckets.blankDrifted.add(row.key);
3249
+ }
3250
+ }
3131
3251
  function classifyRows(params, buckets) {
3132
3252
  for (const row of params.sheet.rows) {
3133
3253
  if (row.translation === "") {
3254
+ trackBlankDrift(row, params, buckets);
3134
3255
  continue;
3135
3256
  }
3136
3257
  if (isUnknownKey(row, params.source, params.target)) {
@@ -3149,9 +3270,20 @@ function classifyRows(params, buckets) {
3149
3270
  }
3150
3271
  }
3151
3272
  }
3273
+ function blankRowBaselineNotice(count) {
3274
+ return {
3275
+ code: "BLANK_ROW_BASELINE_RETAINED",
3276
+ message: `${count} row(s) were left blank for a key whose source changed since the row's baseline was recorded; the prior baseline was kept so the drift keeps being reported.`
3277
+ };
3278
+ }
3152
3279
  function importLocale(params) {
3153
3280
  const diff2 = diffResources(params.source, params.target, { baseline: params.baseline });
3154
- const buckets = { accepted: /* @__PURE__ */ new Map(), mismatches: [], withheld: /* @__PURE__ */ new Set() };
3281
+ const buckets = {
3282
+ accepted: /* @__PURE__ */ new Map(),
3283
+ mismatches: [],
3284
+ withheld: /* @__PURE__ */ new Set(),
3285
+ blankDrifted: /* @__PURE__ */ new Set()
3286
+ };
3155
3287
  classifyRows(params, buckets);
3156
3288
  const rowKeys = new Set(params.sheet.rows.map((row) => row.key));
3157
3289
  const invalidIcuSource = [...new Set(params.sourceInvalidIcuKeys)].filter((key) => rowKeys.has(key)).sort();
@@ -3165,9 +3297,11 @@ function importLocale(params) {
3165
3297
  pruned: [],
3166
3298
  invalidIcuSource,
3167
3299
  integrityMismatches: [...buckets.mismatches].sort(),
3300
+ // A workbook import never calls a provider, so a provider-call failure cannot occur here.
3301
+ providerFailures: [],
3168
3302
  // Plural generation is a translate-flow concern; the manual workbook import never generates forms.
3169
3303
  generated: [],
3170
- notices: []
3304
+ notices: buckets.blankDrifted.size > 0 ? [blankRowBaselineNotice(buckets.blankDrifted.size)] : []
3171
3305
  };
3172
3306
  return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
3173
3307
  }
@@ -3201,21 +3335,19 @@ function mergeAccepted(target, accepted) {
3201
3335
  }
3202
3336
  return merged;
3203
3337
  }
3204
- function computeLockEntries2(source, merged, baseline, withheld) {
3338
+ function computeLockEntries2(source, merged, baseline, accepted) {
3205
3339
  const entries = {};
3206
3340
  for (const key of merged.keys()) {
3207
3341
  const sourceEntry = source.entries.get(key);
3208
3342
  if (sourceEntry === void 0) {
3209
3343
  continue;
3210
3344
  }
3211
- if (withheld.has(key)) {
3212
- const prior = baseline.get(key);
3213
- if (prior !== void 0) {
3214
- entries[key] = prior;
3215
- }
3345
+ if (accepted.has(key)) {
3346
+ entries[key] = contentHash(sourceEntry);
3216
3347
  continue;
3217
3348
  }
3218
- entries[key] = contentHash(sourceEntry);
3349
+ const prior = baseline.get(key);
3350
+ entries[key] = prior !== void 0 ? prior : contentHash(sourceEntry);
3219
3351
  }
3220
3352
  return entries;
3221
3353
  }
@@ -3228,7 +3360,7 @@ async function runSheet(ctx, sheet, lock) {
3228
3360
  }
3229
3361
  const target = await readTarget4(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
3230
3362
  const baseline = baselineFor(lock, sheet.locale);
3231
- const { summary, accepted, withheld } = importLocale({
3363
+ const { summary, accepted } = importLocale({
3232
3364
  sheet,
3233
3365
  source: ctx.source,
3234
3366
  target,
@@ -3252,7 +3384,7 @@ async function runSheet(ctx, sheet, lock) {
3252
3384
  path
3253
3385
  );
3254
3386
  }
3255
- return { summary, lockEntries: computeLockEntries2(ctx.source, merged, baseline, withheld) };
3387
+ return { summary, lockEntries: computeLockEntries2(ctx.source, merged, baseline, accepted) };
3256
3388
  }
3257
3389
  async function importWorkbook(input, deps = {}) {
3258
3390
  const config = input.config;