@verbatra/sdk 0.6.2 → 0.6.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.js CHANGED
@@ -31,27 +31,31 @@ var translationMemorySchema = z.object({
31
31
  function cacheFilePath(cwd) {
32
32
  return resolve(cwd, CACHE_FILE_NAME);
33
33
  }
34
+ var UNUSABLE = { memory: EMPTY_MEMORY, writable: true };
34
35
  async function readTranslationMemory(path, fs) {
35
36
  let read;
36
37
  try {
37
38
  read = await fs.readFileBounded(path, MAX_CACHE_FILE_BYTES);
38
39
  } catch {
39
- return EMPTY_MEMORY;
40
+ return UNUSABLE;
40
41
  }
41
42
  if (read.kind !== "ok") {
42
- return EMPTY_MEMORY;
43
+ return UNUSABLE;
43
44
  }
44
45
  let parsed;
45
46
  try {
46
47
  parsed = JSON.parse(read.content);
47
48
  } catch {
48
- return EMPTY_MEMORY;
49
+ return UNUSABLE;
49
50
  }
50
51
  const result = translationMemorySchema.safeParse(parsed);
51
- if (!result.success || result.data.version !== CURRENT_VERSION) {
52
- return EMPTY_MEMORY;
52
+ if (!result.success) {
53
+ return UNUSABLE;
53
54
  }
54
- return result.data;
55
+ if (result.data.version !== CURRENT_VERSION) {
56
+ return { memory: EMPTY_MEMORY, writable: false };
57
+ }
58
+ return { memory: result.data, writable: true };
55
59
  }
56
60
  function lookupMemory(memory, fingerprint, locale, contentHash2) {
57
61
  return memory.entries[fingerprint]?.[locale]?.[contentHash2];
@@ -103,7 +107,10 @@ async function feedTranslationMemory(cwd, fs, fingerprint, additionsByLocale) {
103
107
  }
104
108
  try {
105
109
  const path = cacheFilePath(cwd);
106
- const memory = await readTranslationMemory(path, fs);
110
+ const { memory, writable } = await readTranslationMemory(path, fs);
111
+ if (!writable) {
112
+ return;
113
+ }
107
114
  await writeTranslationMemory(path, applyAdditions(memory, fingerprint, additionsByLocale), fs);
108
115
  } catch {
109
116
  }
@@ -115,6 +122,18 @@ function defineConfig(config) {
115
122
  }
116
123
 
117
124
  // src/errors.ts
125
+ function errorMessage(error) {
126
+ return error instanceof Error ? error.message : String(error);
127
+ }
128
+ function stringCode(error) {
129
+ if (error instanceof Error && "code" in error && typeof error.code === "string") {
130
+ return error.code;
131
+ }
132
+ return void 0;
133
+ }
134
+ function describeError(error, fallbackCode) {
135
+ return { code: stringCode(error) ?? fallbackCode, message: errorMessage(error) };
136
+ }
118
137
  var SdkError = class extends Error {
119
138
  /** The stable {@link SdkErrorCode} for this failure; branch on this, not the message. */
120
139
  code;
@@ -1912,7 +1931,7 @@ async function loadExplicitWithMeta(explorer, configPath, cwd, fs) {
1912
1931
  try {
1913
1932
  result = await explorer.load(resolved);
1914
1933
  } catch (error) {
1915
- const detail = error instanceof Error ? error.message : String(error);
1934
+ const detail = errorMessage(error);
1916
1935
  throw new SdkError("CONFIG_INVALID", `Failed to load the verbatra configuration: ${detail}`);
1917
1936
  }
1918
1937
  const parsed = parseConfig(result?.config);
@@ -1937,7 +1956,7 @@ async function loadConfigWithMeta(options = {}) {
1937
1956
  try {
1938
1957
  result = await explorer.search(options.cwd);
1939
1958
  } catch (error) {
1940
- const detail = error instanceof Error ? error.message : String(error);
1959
+ const detail = errorMessage(error);
1941
1960
  throw new SdkError("CONFIG_INVALID", `Failed to load the verbatra configuration: ${detail}`);
1942
1961
  }
1943
1962
  if (result === null || result.isEmpty === true) {
@@ -2022,15 +2041,15 @@ async function acquireLock(path, fs, pollIntervalMs, deadline, notify) {
2022
2041
  if (await fs.createExclusive(path, lockPayload())) {
2023
2042
  return;
2024
2043
  }
2044
+ if (notify !== void 0) {
2045
+ await notify();
2046
+ }
2025
2047
  if (Date.now() >= deadline) {
2026
2048
  throw new SdkError(
2027
2049
  "LOCK_CONTENDED",
2028
2050
  `Could not acquire the write lock at ${path}: another process may be holding it. If no verbatra process is currently running, this lock file was likely left behind by one that was killed; delete it and retry.`
2029
2051
  );
2030
2052
  }
2031
- if (notify !== void 0) {
2032
- await notify();
2033
- }
2034
2053
  const jitter = Math.random() * pollIntervalMs;
2035
2054
  await sleep(pollIntervalMs + jitter);
2036
2055
  }
@@ -2620,6 +2639,9 @@ async function fsyncDirBestEffort(path) {
2620
2639
  }
2621
2640
  }
2622
2641
  var nodeOps = {
2642
+ mkdir: async (path) => {
2643
+ await mkdir(path, { recursive: true });
2644
+ },
2623
2645
  writeFile: (path, data) => writeFile(path, data, "utf8"),
2624
2646
  fsyncFile: (path) => fsyncPath(path),
2625
2647
  rename: (from, to) => rename(from, to),
@@ -2636,8 +2658,10 @@ function tempFileName2(path) {
2636
2658
  return join(dirname(path), `.${basename(path)}.tmp-${process.pid}-${Date.now()}-${randomUUID()}`);
2637
2659
  }
2638
2660
  async function atomicWriteFile(path, data, ops = nodeOps) {
2661
+ const directory = dirname(path);
2639
2662
  const tmp = tempFileName2(path);
2640
2663
  try {
2664
+ await ops.mkdir(directory);
2641
2665
  await ops.writeFile(tmp, data);
2642
2666
  await ops.fsyncFile(tmp);
2643
2667
  await ops.rename(tmp, path);
@@ -2646,7 +2670,7 @@ async function atomicWriteFile(path, data, ops = nodeOps) {
2646
2670
  throw error;
2647
2671
  }
2648
2672
  try {
2649
- await ops.fsyncDir(dirname(path));
2673
+ await ops.fsyncDir(directory);
2650
2674
  } catch {
2651
2675
  }
2652
2676
  }
@@ -3309,6 +3333,12 @@ function extractPropertiesPlaceholders(value) {
3309
3333
  var UNICODE_ESCAPE = /^[0-9a-fA-F]{4}$/;
3310
3334
  var LEADING_WHITESPACE = /^[ \t\f]+/;
3311
3335
  var TRAILING_TERMINATOR = /(?:\r\n|\r|\n)$/;
3336
+ function detectLineTerminator(content) {
3337
+ if (content.includes("\r\n")) {
3338
+ return "\r\n";
3339
+ }
3340
+ return content.includes("\r") ? "\r" : "\n";
3341
+ }
3312
3342
  function isPropertiesWhitespace(char) {
3313
3343
  return char === " " || char === " " || char === "\f";
3314
3344
  }
@@ -3514,7 +3544,7 @@ async function readStructure(filePath) {
3514
3544
  outcome = await readBounded2(filePath);
3515
3545
  } catch (error) {
3516
3546
  if (isFileNotFound(error)) {
3517
- return [];
3547
+ return { items: [], terminator: "\n" };
3518
3548
  }
3519
3549
  throw new AdapterError("INVALID_STRUCTURE", "The destination file could not be read.");
3520
3550
  }
@@ -3524,13 +3554,16 @@ async function readStructure(filePath) {
3524
3554
  if (outcome.kind === "too-large") {
3525
3555
  throw new AdapterError("INPUT_TOO_LARGE", "The file exceeds the maximum allowed size.");
3526
3556
  }
3527
- return parseItems(outcome.content);
3557
+ return {
3558
+ items: parseItems(outcome.content),
3559
+ terminator: detectLineTerminator(outcome.content)
3560
+ };
3528
3561
  }
3529
3562
  async function serializePropertiesEntries(entries, filePath) {
3530
- const structure = await readStructure(filePath);
3563
+ const { items, terminator } = await readStructure(filePath);
3531
3564
  const emitted = /* @__PURE__ */ new Set();
3532
3565
  const lines = [];
3533
- for (const item of structure) {
3566
+ for (const item of items) {
3534
3567
  if (item.kind === "raw") {
3535
3568
  lines.push(item.text);
3536
3569
  continue;
@@ -3546,8 +3579,7 @@ async function serializePropertiesEntries(entries, filePath) {
3546
3579
  lines.push(formatEntry(key, entry.value));
3547
3580
  }
3548
3581
  }
3549
- return lines.length === 0 ? "" : `${lines.join("\n")}
3550
- `;
3582
+ return lines.length === 0 ? "" : `${lines.join(terminator)}${terminator}`;
3551
3583
  }
3552
3584
  function createPropertiesAdapter() {
3553
3585
  return createFlatFileAdapter({
@@ -3999,7 +4031,7 @@ async function readSource(config, cwd, fs, adapter) {
3999
4031
  try {
4000
4032
  return await adapter.read(sourcePath, config.sourceLocale);
4001
4033
  } catch (error) {
4002
- const detail = error instanceof Error ? error.message : String(error);
4034
+ const detail = errorMessage(error);
4003
4035
  throw new SdkError(
4004
4036
  "SOURCE_INVALID",
4005
4037
  `The source locale file at ${sourcePath} could not be read: ${detail}`
@@ -4100,6 +4132,9 @@ function gateCandidateValue(sourceEntry, candidateValue, adapter) {
4100
4132
  if (assessValueDegeneracy(sourceEntry.value, candidateValue).degenerate) {
4101
4133
  return { accepted: false, reason: "degenerate" };
4102
4134
  }
4135
+ if (sourceEntry.value.trim() !== "" && candidateValue.trim() === "") {
4136
+ return { accepted: false, reason: "empty" };
4137
+ }
4103
4138
  return { accepted: true };
4104
4139
  }
4105
4140
 
@@ -4296,7 +4331,7 @@ function selectProvider(config, createProvider = buildProvider) {
4296
4331
  try {
4297
4332
  return createProvider(config);
4298
4333
  } catch (error) {
4299
- const detail = error instanceof Error ? error.message : String(error);
4334
+ const detail = errorMessage(error);
4300
4335
  throw new SdkError(
4301
4336
  "PROVIDER_CONSTRUCTION_FAILED",
4302
4337
  `Failed to construct provider "${config.id}": ${detail}`
@@ -4519,13 +4554,6 @@ function budgetExceededNotice(tracker) {
4519
4554
  }
4520
4555
 
4521
4556
  // src/flow/locale-failure.ts
4522
- function describeError(error) {
4523
- if (error instanceof Error) {
4524
- const code = error.code;
4525
- return { code: typeof code === "string" ? code : "LOCALE_FAILED", message: error.message };
4526
- }
4527
- return { code: "LOCALE_FAILED", message: String(error) };
4528
- }
4529
4557
  function failureSummary(locale, error) {
4530
4558
  return {
4531
4559
  locale,
@@ -4545,7 +4573,7 @@ function failureSummary(locale, error) {
4545
4573
  unfilled: [],
4546
4574
  malformedRows: [],
4547
4575
  duplicateKeys: [],
4548
- error: describeError(error)
4576
+ error: describeError(error, "LOCALE_FAILED")
4549
4577
  };
4550
4578
  }
4551
4579
  function deriveLocaleStatus(parts) {
@@ -4949,6 +4977,10 @@ function fanOutAccepted(params, group, acceptedRepresentative, outcome) {
4949
4977
  if (source === void 0) {
4950
4978
  continue;
4951
4979
  }
4980
+ if (!gateCandidateValue(source, acceptedRepresentative.value, params.adapter).accepted) {
4981
+ outcome.integrityMismatches.push(key);
4982
+ continue;
4983
+ }
4952
4984
  outcome.accepted.set(key, { value: acceptedRepresentative.value, source });
4953
4985
  if (flag !== void 0) {
4954
4986
  outcome.reviewFlags.set(key, flag);
@@ -4975,6 +5007,12 @@ function buildRequest3(params, entries) {
4975
5007
  ...params.adapter.comparePlaceholders !== void 0 ? { comparePlaceholders: params.adapter.comparePlaceholders } : {}
4976
5008
  };
4977
5009
  }
5010
+ async function shouldWriteTarget(params, path, changed) {
5011
+ if (changed.accepted > 0 || changed.pruned > 0 || changed.generated > 0) {
5012
+ return true;
5013
+ }
5014
+ return !await params.fs.fileExists(path);
5015
+ }
4978
5016
  async function runLocale(params) {
4979
5017
  const target = await readTargetResource({
4980
5018
  cwd: params.cwd,
@@ -5060,15 +5098,22 @@ async function runLocale(params) {
5060
5098
  merged.set(form.targetKey, { ...form.entry, namespace: target.namespace });
5061
5099
  }
5062
5100
  const path = localeFilePath(params.cwd, params.filesPattern, params.targetLocale);
5063
- await params.adapter.write(
5064
- {
5065
- locale: params.targetLocale,
5066
- namespace: target.namespace,
5067
- format: params.format,
5068
- entries: merged
5069
- },
5070
- path
5071
- );
5101
+ const writeNeeded = await shouldWriteTarget(params, path, {
5102
+ accepted: accepted.size,
5103
+ pruned: pruned.length,
5104
+ generated: generation.accepted.length
5105
+ });
5106
+ if (writeNeeded) {
5107
+ await params.adapter.write(
5108
+ {
5109
+ locale: params.targetLocale,
5110
+ namespace: target.namespace,
5111
+ format: params.format,
5112
+ entries: merged
5113
+ },
5114
+ path
5115
+ );
5116
+ }
5072
5117
  const pluralNotices = params.generatePlurals ? pluralNoticeFor(params, merged) : sdkNotices;
5073
5118
  const notices = [
5074
5119
  ...pluralNotices,
@@ -5340,14 +5385,21 @@ async function createRunCacheState(input, config, cwd, dryRun, fs) {
5340
5385
  if (dryRun || input.cache === false) {
5341
5386
  return void 0;
5342
5387
  }
5343
- return {
5344
- memory: await readTranslationMemory(cacheFilePath(cwd), fs),
5345
- fingerprint: computeFingerprint(config),
5346
- additions: /* @__PURE__ */ new Map()
5388
+ const { memory, writable } = await readTranslationMemory(cacheFilePath(cwd), fs);
5389
+ return { memory, writable, fingerprint: computeFingerprint(config), additions: /* @__PURE__ */ new Map() };
5390
+ }
5391
+ function withCacheNotices(summaries, cache) {
5392
+ if (cache === void 0 || cache.writable) {
5393
+ return [...summaries];
5394
+ }
5395
+ const notice = {
5396
+ code: "CACHE_VERSION_UNRECOGNIZED",
5397
+ message: `${CACHE_FILE_NAME} carries a version this build does not recognize, so it was written by a newer verbatra. It was left untouched and this run used no cache. Upgrade verbatra, or delete the file to rebuild it in this build's format.`
5347
5398
  };
5399
+ return summaries.map((summary) => ({ ...summary, notices: [...summary.notices, notice] }));
5348
5400
  }
5349
5401
  async function recordCacheAdditions(cwd, cache, fs) {
5350
- if (cache === void 0 || cache.additions.size === 0) {
5402
+ if (cache === void 0 || cache.additions.size === 0 || !cache.writable) {
5351
5403
  return;
5352
5404
  }
5353
5405
  try {
@@ -5434,18 +5486,25 @@ async function runLocaleAt(context, targetLocales, localeIndex, runOne, results)
5434
5486
  context.onProgress?.({
5435
5487
  type: "locale-finished",
5436
5488
  locale: targetLocale,
5437
- translated: summary.translated.length
5489
+ translated: summary.translated.length,
5490
+ localeIndex,
5491
+ totalLocales: targetLocales.length
5438
5492
  });
5439
5493
  }
5440
5494
  async function runLocalesWithProgress(context, targetLocales, runOne, concurrency) {
5441
5495
  const totalLocales = targetLocales.length;
5442
5496
  const results = new Array(totalLocales);
5443
5497
  let nextIndex = 0;
5498
+ let abort;
5444
5499
  async function worker() {
5445
- while (nextIndex < totalLocales) {
5500
+ while (abort === void 0 && nextIndex < totalLocales) {
5446
5501
  const localeIndex = nextIndex;
5447
5502
  nextIndex += 1;
5448
- await runLocaleAt(context, targetLocales, localeIndex, runOne, results);
5503
+ try {
5504
+ await runLocaleAt(context, targetLocales, localeIndex, runOne, results);
5505
+ } catch (error) {
5506
+ abort ??= { reason: error };
5507
+ }
5449
5508
  }
5450
5509
  }
5451
5510
  const workerCount = Math.min(concurrency, totalLocales);
@@ -5454,6 +5513,9 @@ async function runLocalesWithProgress(context, targetLocales, runOne, concurrenc
5454
5513
  workers.push(worker());
5455
5514
  }
5456
5515
  await Promise.all(workers);
5516
+ if (abort !== void 0) {
5517
+ throw abort.reason;
5518
+ }
5457
5519
  return results.filter((summary) => summary !== void 0);
5458
5520
  }
5459
5521
  async function runAllLocalesDry(context, targetLocales, concurrency) {
@@ -5530,7 +5592,8 @@ async function translate2(input, deps = {}) {
5530
5592
  };
5531
5593
  const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales, concurrency) : await runAllLocalesLive(context, config.targetLocales, concurrency);
5532
5594
  input.onProgress?.({ type: "run-finished", localesCompleted: summaries.length });
5533
- const { succeeded, partial, failed } = partition(summaries);
5595
+ const locales = withCacheNotices(summaries, cache);
5596
+ const { succeeded, partial, failed } = partition(locales);
5534
5597
  const usage = summaries.reduce(
5535
5598
  (total, summary2) => combineUsage(total, summary2.usage),
5536
5599
  void 0
@@ -5538,7 +5601,7 @@ async function translate2(input, deps = {}) {
5538
5601
  const budgetSummary = toBudgetSummary(budget);
5539
5602
  const summary = {
5540
5603
  dryRun,
5541
- locales: summaries,
5604
+ locales,
5542
5605
  succeeded,
5543
5606
  partial,
5544
5607
  failed,
@@ -6110,12 +6173,12 @@ function classifyClear(row, sourceEntry, buckets) {
6110
6173
  buckets.withheld.add(row.key);
6111
6174
  return;
6112
6175
  }
6113
- buckets.accepted.set(row.key, { value: "", source: sourceEntry });
6176
+ buckets.accepted.set(row.key, { value: "", source: sourceEntry, cleared: true });
6114
6177
  }
6115
- function classifyRows(params, buckets) {
6178
+ function classifyRows(params, buckets, liveCandidates) {
6116
6179
  for (const row of params.sheet.rows) {
6117
6180
  if (row.translation === "") {
6118
- if (row.status === "changed") {
6181
+ if (liveCandidates.has(row.key)) {
6119
6182
  buckets.unfilled.push(row.key);
6120
6183
  }
6121
6184
  trackBlankDrift(row, params, buckets);
@@ -6134,7 +6197,11 @@ function classifyRows(params, buckets) {
6134
6197
  }
6135
6198
  const reason = judge(row, sourceEntry, params.adapter);
6136
6199
  if (reason === void 0) {
6137
- buckets.accepted.set(row.key, { value: row.translation, source: sourceEntry });
6200
+ buckets.accepted.set(row.key, {
6201
+ value: row.translation,
6202
+ source: sourceEntry,
6203
+ cleared: false
6204
+ });
6138
6205
  } else {
6139
6206
  buckets.mismatches.push(row.key);
6140
6207
  buckets.withheld.add(row.key);
@@ -6156,7 +6223,7 @@ function importLocale(params) {
6156
6223
  blankDrifted: /* @__PURE__ */ new Set(),
6157
6224
  unfilled: []
6158
6225
  };
6159
- classifyRows(params, buckets);
6226
+ classifyRows(params, buckets, /* @__PURE__ */ new Set([...diff2.missing, ...diff2.changed]));
6160
6227
  const rowKeys = new Set(params.sheet.rows.map((row) => row.key));
6161
6228
  const invalidIcuSource = [...new Set(params.sourceInvalidIcuKeys)].filter((key) => rowKeys.has(key)).sort();
6162
6229
  const translated = [...buckets.accepted.keys()].sort();
@@ -6214,8 +6281,10 @@ function mergeAccepted(target, accepted) {
6214
6281
  }
6215
6282
  function sheetCacheAdditions(accepted) {
6216
6283
  const record = {};
6217
- for (const [, { value, source }] of accepted) {
6218
- record[contentHash(source)] = value;
6284
+ for (const [, { value, source, cleared }] of accepted) {
6285
+ if (!cleared) {
6286
+ record[contentHash(source)] = value;
6287
+ }
6219
6288
  }
6220
6289
  return record;
6221
6290
  }
@@ -6304,7 +6373,7 @@ async function importWorkbook(input, deps = {}) {
6304
6373
  try {
6305
6374
  data = await readWorkbook(bytes);
6306
6375
  } catch (error) {
6307
- throw new SdkError("SOURCE_INVALID", error.message);
6376
+ throw new SdkError("SOURCE_INVALID", errorMessage(error));
6308
6377
  }
6309
6378
  const lock = await readLockFile(lockFilePath(cwd), fs);
6310
6379
  const ctx = {
@@ -6390,17 +6459,11 @@ function defaultRunTranslate(deps) {
6390
6459
 
6391
6460
  // src/watch/watch.ts
6392
6461
  var DEFAULT_DEBOUNCE_MS = 300;
6393
- function describeError2(error) {
6394
- if (error instanceof Error) {
6395
- const code = error.code;
6396
- return { code: typeof code === "string" ? code : "WATCH_RUN_FAILED", message: error.message };
6397
- }
6398
- return { code: "WATCH_RUN_FAILED", message: String(error) };
6399
- }
6400
6462
  async function watch(input, deps = {}) {
6401
6463
  const cwd = input.cwd ?? process.cwd();
6402
6464
  const debounceMs = input.debounceMs ?? DEFAULT_DEBOUNCE_MS;
6403
6465
  const fs = deps.fs ?? defaultFs;
6466
+ resolveRunConcurrency(input.concurrency, false, input.config);
6404
6467
  const sourcePath = localeFilePath(cwd, input.config.files.pattern, input.config.sourceLocale);
6405
6468
  if (!await fs.fileExists(sourcePath)) {
6406
6469
  throw new SdkError(
@@ -6427,7 +6490,7 @@ async function watch(input, deps = {}) {
6427
6490
  try {
6428
6491
  input.onRun({ status: "succeeded", summary: await runTranslate(runInput) });
6429
6492
  } catch (error) {
6430
- input.onRun({ status: "failed", error: describeError2(error) });
6493
+ input.onRun({ status: "failed", error: describeError(error, "WATCH_RUN_FAILED") });
6431
6494
  }
6432
6495
  }
6433
6496
  function startRun() {