@verbatra/sdk 0.8.0 → 0.9.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/dist/index.cjs CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  var path = require('path');
4
4
  var zod = require('zod');
5
+ var crypto = require('crypto');
6
+ var promises = require('fs/promises');
5
7
  var fs = require('fs');
6
8
  var cosmiconfig = require('cosmiconfig');
7
9
  var cosmiconfigTypescriptLoader = require('cosmiconfig-typescript-loader');
8
- var crypto = require('crypto');
9
- var promises = require('fs/promises');
10
10
  var Anthropic = require('@anthropic-ai/sdk');
11
11
  var deepl = require('deepl-node');
12
12
  var module$1 = require('module');
@@ -297,52 +297,238 @@ var defaultFs = {
297
297
  await promises.mkdir(path, { recursive: true });
298
298
  }
299
299
  };
300
+ var LOCAL_DIR_NAME = ".verbatra-local";
301
+ var LOCK_FILE_GUARD_STEM = "_lockfile";
302
+ var GLOSSARY_GUARD_STEM = "_glossary";
303
+ var DEFAULT_POLL_INTERVAL_MS = 100;
304
+ var DEFAULT_ACQUIRE_TIMEOUT_MS = 10 * 6e4;
305
+ var WAIT_NOTICE_INTERVAL_MS = 1e3;
306
+ var MAX_LOCK_PAYLOAD_BYTES = 64 * 1024;
307
+ function lockPath(cwd, stem) {
308
+ return path.resolve(cwd, LOCAL_DIR_NAME, "locks", `${stem}.lock`);
309
+ }
310
+ function localeLockPath(cwd, locale) {
311
+ return lockPath(cwd, locale);
312
+ }
313
+ function lockFileGuardPath(cwd) {
314
+ return lockPath(cwd, LOCK_FILE_GUARD_STEM);
315
+ }
316
+ function glossaryGuardPath(cwd) {
317
+ return lockPath(cwd, GLOSSARY_GUARD_STEM);
318
+ }
319
+ function sleep(ms) {
320
+ return new Promise((res) => {
321
+ setTimeout(res, ms);
322
+ });
323
+ }
324
+ function lockPayload() {
325
+ return JSON.stringify({ pid: process.pid, acquiredAt: (/* @__PURE__ */ new Date()).toISOString() });
326
+ }
327
+ function parseHolder(read) {
328
+ if (read.kind !== "ok") {
329
+ return void 0;
330
+ }
331
+ let parsed;
332
+ try {
333
+ parsed = JSON.parse(read.content);
334
+ } catch {
335
+ return void 0;
336
+ }
337
+ if (typeof parsed !== "object" || parsed === null) {
338
+ return void 0;
339
+ }
340
+ const record = parsed;
341
+ const holder = {};
342
+ if (typeof record.pid === "number") {
343
+ holder.pid = record.pid;
344
+ }
345
+ if (typeof record.acquiredAt === "string") {
346
+ holder.acquiredAt = record.acquiredAt;
347
+ }
348
+ return holder;
349
+ }
350
+ function makeWaitNotifier(path, fs, onWait, start) {
351
+ let holder;
352
+ let holderRead = false;
353
+ let lastEmit;
354
+ return async () => {
355
+ if (!holderRead) {
356
+ holderRead = true;
357
+ holder = parseHolder(await fs.readFileBounded(path, MAX_LOCK_PAYLOAD_BYTES));
358
+ }
359
+ const elapsedMs = Date.now() - start;
360
+ if (lastEmit !== void 0 && elapsedMs - lastEmit < WAIT_NOTICE_INTERVAL_MS) {
361
+ return;
362
+ }
363
+ lastEmit = elapsedMs;
364
+ onWait({ lockPath: path, elapsedMs, ...holder !== void 0 ? { holder } : {} });
365
+ };
366
+ }
367
+ async function acquireLock(path, fs, pollIntervalMs, deadline, notify) {
368
+ for (; ; ) {
369
+ if (await fs.createExclusive(path, lockPayload())) {
370
+ return;
371
+ }
372
+ if (notify !== void 0) {
373
+ await notify();
374
+ }
375
+ if (Date.now() >= deadline) {
376
+ throw new SdkError(
377
+ "LOCK_CONTENDED",
378
+ `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.`
379
+ );
380
+ }
381
+ const jitter = Math.random() * pollIntervalMs;
382
+ await sleep(pollIntervalMs + jitter);
383
+ }
384
+ }
385
+ async function withFileLock(path, fs, fn, options) {
386
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
387
+ const acquireTimeoutMs = options.acquireTimeoutMs ?? DEFAULT_ACQUIRE_TIMEOUT_MS;
388
+ const start = Date.now();
389
+ const notify = options.onWait !== void 0 ? makeWaitNotifier(path, fs, options.onWait, start) : void 0;
390
+ await acquireLock(path, fs, pollIntervalMs, start + acquireTimeoutMs, notify);
391
+ try {
392
+ return await fn();
393
+ } finally {
394
+ await fs.deleteFile(path);
395
+ }
396
+ }
397
+ async function withLocaleWriteLock(cwd, locale, fs, fn, options = {}) {
398
+ return withFileLock(localeLockPath(cwd, locale), fs, fn, options);
399
+ }
400
+ async function withLockFileGuard(cwd, fs, fn, options = {}) {
401
+ return withFileLock(lockFileGuardPath(cwd), fs, fn, options);
402
+ }
403
+ async function withGlossaryGuard(cwd, fs, fn, options = {}) {
404
+ return withFileLock(glossaryGuardPath(cwd), fs, fn, options);
405
+ }
406
+
407
+ // src/config/glossary-file.ts
300
408
  var MAX_GLOSSARY_FILE_BYTES = 1024 * 1024;
301
- var glossaryRecordSchema = zod.z.record(zod.z.string(), zod.z.string());
409
+ var DEFAULT_INDENT = " ";
410
+ var INDENT_PATTERN = /^([ \t]+)"/m;
302
411
  var BOM = "\uFEFF";
303
412
  var REPLACEMENT_CHARACTER = "\uFFFD";
304
413
  var NUL = "\0";
414
+ var glossaryRecordSchema = zod.z.record(zod.z.string(), zod.z.string());
305
415
  function stripBom(content) {
306
416
  return content.startsWith(BOM) ? content.slice(BOM.length) : content;
307
417
  }
308
418
  function looksLikeInvalidEncoding(content) {
309
419
  return content.startsWith(REPLACEMENT_CHARACTER) || content.includes(NUL);
310
420
  }
311
- async function readGlossaryFile(resolvedPath, fs) {
312
- const read = await fs.readFileBounded(resolvedPath, MAX_GLOSSARY_FILE_BYTES);
421
+ function detectIndent(content) {
422
+ return INDENT_PATTERN.exec(content)?.[1] ?? DEFAULT_INDENT;
423
+ }
424
+ async function readGlossaryDocument(path, fs) {
425
+ const read = await fs.readFileBounded(path, MAX_GLOSSARY_FILE_BYTES);
313
426
  if (read.kind === "missing") {
314
427
  throw new SdkError(
315
428
  "CONFIG_INVALID",
316
- `The glossary file at ${resolvedPath} was not found or could not be read.`
429
+ `The glossary file at ${path} was not found or could not be read.`
317
430
  );
318
431
  }
319
432
  if (read.kind === "too-large") {
320
433
  throw new SdkError(
321
434
  "CONFIG_INVALID",
322
- `The glossary file at ${resolvedPath} exceeds the maximum allowed size of ${MAX_GLOSSARY_FILE_BYTES} bytes.`
435
+ `The glossary file at ${path} exceeds the maximum allowed size of ${MAX_GLOSSARY_FILE_BYTES} bytes.`
323
436
  );
324
437
  }
325
438
  const content = stripBom(read.content);
326
439
  if (looksLikeInvalidEncoding(content)) {
327
- throw new SdkError(
328
- "CONFIG_INVALID",
329
- `The glossary file at ${resolvedPath} must be UTF-8 encoded.`
330
- );
440
+ throw new SdkError("CONFIG_INVALID", `The glossary file at ${path} must be UTF-8 encoded.`);
331
441
  }
332
442
  let parsed;
333
443
  try {
334
444
  parsed = JSON.parse(content);
335
445
  } catch {
336
- throw new SdkError("CONFIG_INVALID", `The glossary file at ${resolvedPath} is not valid JSON.`);
446
+ throw new SdkError("CONFIG_INVALID", `The glossary file at ${path} is not valid JSON.`);
337
447
  }
338
448
  const result = glossaryRecordSchema.safeParse(parsed);
339
449
  if (!result.success) {
340
450
  throw new SdkError(
341
451
  "CONFIG_INVALID",
342
- `The glossary file at ${resolvedPath} must contain a flat object of string keys to string values.`
452
+ `The glossary file at ${path} must contain a flat object of string keys to string values.`
453
+ );
454
+ }
455
+ return {
456
+ entries: result.data,
457
+ indent: detectIndent(content),
458
+ trailingNewline: content.endsWith("\n") ? "\n" : ""
459
+ };
460
+ }
461
+ async function readGlossaryRecord(path, fs) {
462
+ return (await readGlossaryDocument(path, fs)).entries;
463
+ }
464
+ function glossaryFilePath(glossary) {
465
+ if (glossary.source !== "file") {
466
+ throw new SdkError(
467
+ "GLOSSARY_NOT_FILE_BACKED",
468
+ `The glossary is ${glossary.source === "inline" ? "written inline in the config" : "not configured"}, so there is no glossary file to work with. Point the config's glossary at a JSON file first.`
469
+ );
470
+ }
471
+ return glossary.path;
472
+ }
473
+ function assertWritableTerm(term, translation) {
474
+ if (term.trim().length === 0) {
475
+ throw new SdkError("CONFIG_INVALID", "A glossary term must not be blank.");
476
+ }
477
+ if (translation !== null && translation.trim().length === 0) {
478
+ throw new SdkError(
479
+ "CONFIG_INVALID",
480
+ `The translation for the glossary term "${term}" must not be blank. Remove the term instead.`
481
+ );
482
+ }
483
+ }
484
+ function applyTerm(current, term, translation) {
485
+ const pairs = Object.entries(current);
486
+ const index = pairs.findIndex(([existing]) => existing === term);
487
+ if (translation === null) {
488
+ if (index >= 0) {
489
+ pairs.splice(index, 1);
490
+ }
491
+ } else if (index >= 0) {
492
+ pairs[index] = [term, translation];
493
+ } else {
494
+ pairs.push([term, translation]);
495
+ }
496
+ return Object.fromEntries(pairs);
497
+ }
498
+ function serializeGlossary(entries, document, path) {
499
+ const serialized = `${JSON.stringify(entries, null, document.indent)}${document.trailingNewline}`;
500
+ if (Buffer.byteLength(serialized, "utf8") > MAX_GLOSSARY_FILE_BYTES) {
501
+ throw new SdkError(
502
+ "CONFIG_INVALID",
503
+ `The updated glossary file at ${path} would exceed the maximum allowed size of ${MAX_GLOSSARY_FILE_BYTES} bytes.`
504
+ );
505
+ }
506
+ return serialized;
507
+ }
508
+ async function writeGlossary(path, serialized, fs) {
509
+ try {
510
+ await fs.writeFile(path, serialized);
511
+ } catch (error) {
512
+ throw new SdkError(
513
+ "GLOSSARY_UNWRITABLE",
514
+ `The glossary file at ${path} could not be written: ${error instanceof Error ? error.message : String(error)}`
343
515
  );
344
516
  }
345
- return result.data;
517
+ }
518
+ async function readGlossaryFile(input, deps = {}) {
519
+ return readGlossaryRecord(glossaryFilePath(input.glossary), deps.fs ?? defaultFs);
520
+ }
521
+ async function updateGlossaryTerm(input, deps = {}) {
522
+ const path = glossaryFilePath(input.glossary);
523
+ assertWritableTerm(input.term, input.translation);
524
+ const fs = deps.fs ?? defaultFs;
525
+ const cwd = input.cwd ?? process.cwd();
526
+ return withGlossaryGuard(cwd, fs, async () => {
527
+ const document = await readGlossaryDocument(path, fs);
528
+ const entries = applyTerm(document.entries, input.term, input.translation);
529
+ await writeGlossary(path, serializeGlossary(entries, document, path), fs);
530
+ return entries;
531
+ });
346
532
  }
347
533
  async function resolveGlossary(glossary, baseDir, fs) {
348
534
  if (glossary === void 0) {
@@ -350,7 +536,7 @@ async function resolveGlossary(glossary, baseDir, fs) {
350
536
  }
351
537
  if (typeof glossary === "string") {
352
538
  const resolvedPath = path.resolve(baseDir, glossary);
353
- const record = await readGlossaryFile(resolvedPath, fs);
539
+ const record = await readGlossaryRecord(resolvedPath, fs);
354
540
  return { glossary: record, provenance: { source: "file", path: resolvedPath } };
355
541
  }
356
542
  return { glossary, provenance: { source: "inline" } };
@@ -1001,6 +1187,66 @@ function isAbortError(error, signal) {
1001
1187
  }
1002
1188
  return isAbortShapedError(error);
1003
1189
  }
1190
+ var REFUSED = {
1191
+ cause: "the connection was refused",
1192
+ remedy: "Check that the endpoint is running and that the configured host and port are right, then run again."
1193
+ };
1194
+ var UNRESOLVED = {
1195
+ cause: "the host name could not be resolved",
1196
+ remedy: "Check the spelling of the configured endpoint and your DNS or network access."
1197
+ };
1198
+ var CLOSED = {
1199
+ cause: "the connection was closed before a reply arrived",
1200
+ remedy: "Retry, and check any proxy or firewall between you and the endpoint."
1201
+ };
1202
+ var UNREACHABLE = {
1203
+ cause: "the host could not be reached",
1204
+ remedy: "Check your network access and any proxy or firewall between you and the endpoint."
1205
+ };
1206
+ var UNTRUSTED_CERTIFICATE = {
1207
+ cause: "the endpoint's TLS certificate could not be verified",
1208
+ remedy: "Check the certificate the endpoint serves, or point at a host your system already trusts."
1209
+ };
1210
+ var NETWORK_CAUSE_BY_CODE = {
1211
+ ECONNREFUSED: REFUSED,
1212
+ ENOTFOUND: UNRESOLVED,
1213
+ EAI_AGAIN: UNRESOLVED,
1214
+ ECONNRESET: CLOSED,
1215
+ ECONNABORTED: CLOSED,
1216
+ EPIPE: CLOSED,
1217
+ ETIMEDOUT: UNREACHABLE,
1218
+ EHOSTUNREACH: UNREACHABLE,
1219
+ ENETUNREACH: UNREACHABLE,
1220
+ CERT_HAS_EXPIRED: UNTRUSTED_CERTIFICATE,
1221
+ DEPTH_ZERO_SELF_SIGNED_CERT: UNTRUSTED_CERTIFICATE,
1222
+ UNABLE_TO_VERIFY_LEAF_SIGNATURE: UNTRUSTED_CERTIFICATE,
1223
+ SELF_SIGNED_CERT_IN_CHAIN: UNTRUSTED_CERTIFICATE
1224
+ };
1225
+ var MAX_CAUSE_DEPTH = 4;
1226
+ function ownCode(error) {
1227
+ if (typeof error !== "object" || error === null || !("code" in error)) {
1228
+ return void 0;
1229
+ }
1230
+ const code = error.code;
1231
+ return typeof code === "string" ? code : void 0;
1232
+ }
1233
+ function nextCause(error) {
1234
+ return typeof error === "object" && error !== null && "cause" in error ? error.cause : void 0;
1235
+ }
1236
+ function findNetworkCause(error) {
1237
+ let current = error;
1238
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH && current !== void 0; depth += 1) {
1239
+ const code = ownCode(current);
1240
+ if (code !== void 0 && Object.hasOwn(NETWORK_CAUSE_BY_CODE, code)) {
1241
+ return NETWORK_CAUSE_BY_CODE[code];
1242
+ }
1243
+ current = nextCause(current);
1244
+ }
1245
+ return void 0;
1246
+ }
1247
+ function describeNetworkCause(cause) {
1248
+ return `${cause.cause}. ${cause.remedy}`;
1249
+ }
1004
1250
  var PROVIDER_CALL_FAILED_MESSAGE = "The translation provider request failed.";
1005
1251
  var RATE_LIMITED_MESSAGE = "The translation provider rate-limited this request.";
1006
1252
  var TIMEOUT_MESSAGE = "The translation provider request timed out.";
@@ -1013,7 +1259,19 @@ var MESSAGE_BY_CODE = {
1013
1259
  PROVIDER_UNAVAILABLE: PROVIDER_UNAVAILABLE_MESSAGE,
1014
1260
  PROVIDER_ERROR: PROVIDER_CALL_FAILED_MESSAGE
1015
1261
  };
1016
- async function guardProviderCall(call, signal) {
1262
+ function providerErrorMessage(error, endpointHost) {
1263
+ const cause = findNetworkCause(error);
1264
+ if (cause === void 0 && endpointHost === void 0) {
1265
+ return PROVIDER_CALL_FAILED_MESSAGE;
1266
+ }
1267
+ const where = endpointHost === void 0 ? "" : ` to ${endpointHost}`;
1268
+ const why = cause === void 0 ? "" : `: ${describeNetworkCause(cause)}`;
1269
+ return `The translation provider request${where} failed${why === "" ? "." : why}`;
1270
+ }
1271
+ function messageFor(code, error, context) {
1272
+ return code === "PROVIDER_ERROR" ? providerErrorMessage(error, context?.endpointHost) : MESSAGE_BY_CODE[code];
1273
+ }
1274
+ async function guardProviderCall(call, signal, context) {
1017
1275
  signal?.throwIfAborted();
1018
1276
  try {
1019
1277
  return await call();
@@ -1022,7 +1280,7 @@ async function guardProviderCall(call, signal) {
1022
1280
  throw error;
1023
1281
  }
1024
1282
  const code = classifyProviderError(error);
1025
- throw new ProviderError(code, MESSAGE_BY_CODE[code]);
1283
+ throw new ProviderError(code, messageFor(code, error, context));
1026
1284
  }
1027
1285
  }
1028
1286
  var DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
@@ -1032,7 +1290,7 @@ function requestTimedOutMessage(timeoutMs) {
1032
1290
  function combineSignals(caller, timeout) {
1033
1291
  return caller === void 0 ? timeout : AbortSignal.any([caller, timeout]);
1034
1292
  }
1035
- async function withRequestTimeout(timeoutMs, callerSignal, call) {
1293
+ async function withRequestTimeout(timeoutMs, callerSignal, call, context) {
1036
1294
  const timeoutController = new AbortController();
1037
1295
  const signal = combineSignals(callerSignal, timeoutController.signal);
1038
1296
  let timer;
@@ -1043,7 +1301,7 @@ async function withRequestTimeout(timeoutMs, callerSignal, call) {
1043
1301
  }, timeoutMs);
1044
1302
  });
1045
1303
  try {
1046
- return await Promise.race([guardProviderCall(() => call(signal), signal), timedOut]);
1304
+ return await Promise.race([guardProviderCall(() => call(signal), signal, context), timedOut]);
1047
1305
  } finally {
1048
1306
  if (timer !== void 0) {
1049
1307
  clearTimeout(timer);
@@ -1777,22 +2035,22 @@ var HOSTED_PROVIDER_ENV_VARS = new Set(
1777
2035
  function isNotHostedProviderEnvVar(value) {
1778
2036
  return !HOSTED_PROVIDER_ENV_VARS.has(value.toUpperCase());
1779
2037
  }
1780
- function isHttpOrHttpsUrl(value) {
1781
- try {
1782
- const { protocol } = new URL(value);
1783
- return protocol === "http:" || protocol === "https:";
1784
- } catch {
1785
- return false;
1786
- }
1787
- }
2038
+ var HTTP_OR_HTTPS_SCHEME = /^https?:\/\//i;
1788
2039
  var openAiCompatibleConfigSchema = zod.z.object({
1789
- baseUrl: zod.z.url({ message: "baseUrl must be a valid absolute URL." }).refine(isHttpOrHttpsUrl, { message: "baseUrl must use the http or https scheme." }),
2040
+ baseUrl: zod.z.url({ message: "baseUrl must be a valid absolute URL." }).regex(HTTP_OR_HTTPS_SCHEME, { message: "baseUrl must use the http or https scheme." }),
1790
2041
  model: zod.z.string().min(1),
1791
2042
  maxOutputTokens: zod.z.number().int().positive(),
1792
2043
  apiKeyEnvVar: zod.z.string().min(1).refine(isNotHostedProviderEnvVar, {
1793
2044
  message: "apiKeyEnvVar must not name a hosted provider's environment variable."
1794
2045
  }).optional()
1795
2046
  }).extend(requestTimeoutConfigSchema.shape);
2047
+ function endpointContextOf(baseUrl) {
2048
+ try {
2049
+ return { endpointHost: new URL(baseUrl).host };
2050
+ } catch {
2051
+ return void 0;
2052
+ }
2053
+ }
1796
2054
  function createDefaultClient5(config) {
1797
2055
  const sdk = new OpenAI__default.default({
1798
2056
  apiKey: resolveOpenAiCompatibleKey(config.apiKeyEnvVar),
@@ -1824,19 +2082,21 @@ function createOpenAiCompatibleProvider(config, deps = {}) {
1824
2082
  }
1825
2083
  function createMechanism4(client, config) {
1826
2084
  const timeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
2085
+ const endpoint = endpointContextOf(config.baseUrl);
1827
2086
  return {
1828
2087
  translate: async ({ payloadJson, signal }) => {
1829
2088
  const body = buildOpenAiRequest(config, payloadJson, "strict-schema", "max_tokens");
1830
- const completion = await callClient4(client, body, timeoutMs, signal);
2089
+ const completion = await callClient4(client, body, timeoutMs, signal, endpoint);
1831
2090
  return extractOpenAiResult(completion, true);
1832
2091
  }
1833
2092
  };
1834
2093
  }
1835
- function callClient4(client, body, timeoutMs, signal) {
2094
+ function callClient4(client, body, timeoutMs, signal, endpoint) {
1836
2095
  return withRequestTimeout(
1837
2096
  timeoutMs,
1838
2097
  signal,
1839
- (requestSignal) => client.chat.completions.create(body, { signal: requestSignal })
2098
+ (requestSignal) => client.chat.completions.create(body, { signal: requestSignal }),
2099
+ endpoint
1840
2100
  );
1841
2101
  }
1842
2102
  var SCAFFOLD_MODELS = {
@@ -1866,6 +2126,10 @@ var providerFactories = {
1866
2126
  deepl: (options) => createDeepLProvider(options),
1867
2127
  "openai-compatible": (options) => createOpenAiCompatibleProvider(options)
1868
2128
  };
2129
+ var PROVIDER_IDS = Object.keys(providerFactories);
2130
+ function hasProviderFactory(id) {
2131
+ return Object.hasOwn(providerFactories, id);
2132
+ }
1869
2133
  function buildProvider(config) {
1870
2134
  const create = providerFactories[config.id];
1871
2135
  return create(config.options);
@@ -1886,11 +2150,12 @@ function findCaseInsensitiveDuplicate(locales) {
1886
2150
  return void 0;
1887
2151
  }
1888
2152
  var verbatraConfigSchema = zod.z.strictObject({
2153
+ $schema: zod.z.string().optional(),
1889
2154
  sourceLocale: zod.z.string().min(1),
1890
2155
  targetLocales: zod.z.array(zod.z.string().min(1)).min(1),
1891
2156
  format: supportedFormatSchema,
1892
2157
  files: zod.z.strictObject({
1893
- pattern: zod.z.string().min(1),
2158
+ pattern: zod.z.string().min(1).regex(/\{locale\}/, { message: `files.pattern must contain the ${LOCALE_TOKEN} token` }),
1894
2159
  localeStyle: zod.z.enum(LOCALE_STYLES).optional()
1895
2160
  }),
1896
2161
  provider: providerConfigSchema,
@@ -1912,9 +2177,6 @@ var verbatraConfigSchema = zod.z.strictObject({
1912
2177
  return `targetLocales must not contain case-insensitively duplicate locales: "${duplicate}"`;
1913
2178
  },
1914
2179
  path: ["targetLocales"]
1915
- }).refine((config) => config.files.pattern.includes(LOCALE_TOKEN), {
1916
- message: `files.pattern must contain the ${LOCALE_TOKEN} token`,
1917
- path: ["files", "pattern"]
1918
2180
  });
1919
2181
 
1920
2182
  // src/config/load-config.ts
@@ -2059,114 +2321,13 @@ function createLocalePathResolver(cwd, config) {
2059
2321
  const style = config.files.localeStyle ?? DEFAULT_LOCALE_STYLE;
2060
2322
  const pattern = config.files.pattern;
2061
2323
  validatePattern(pattern, style);
2062
- const pathFor = (locale) => path.resolve(cwd, expandPattern(pattern, safeSpelling(locale, style, config.sourceLocale)));
2063
- const forward = buildForwardMap(config, pathFor);
2064
- return {
2065
- pathFor,
2066
- localeFor: (absolutePath) => forward.get(path.resolve(cwd, absolutePath))
2067
- };
2068
- }
2069
- var LOCAL_DIR_NAME = ".verbatra-local";
2070
- var LOCK_FILE_GUARD_STEM = "_lockfile";
2071
- var DEFAULT_POLL_INTERVAL_MS = 100;
2072
- var DEFAULT_ACQUIRE_TIMEOUT_MS = 10 * 6e4;
2073
- var WAIT_NOTICE_INTERVAL_MS = 1e3;
2074
- var MAX_LOCK_PAYLOAD_BYTES = 64 * 1024;
2075
- function lockPath(cwd, stem) {
2076
- return path.resolve(cwd, LOCAL_DIR_NAME, "locks", `${stem}.lock`);
2077
- }
2078
- function localeLockPath(cwd, locale) {
2079
- return lockPath(cwd, locale);
2080
- }
2081
- function lockFileGuardPath(cwd) {
2082
- return lockPath(cwd, LOCK_FILE_GUARD_STEM);
2083
- }
2084
- function sleep(ms) {
2085
- return new Promise((res) => {
2086
- setTimeout(res, ms);
2087
- });
2088
- }
2089
- function lockPayload() {
2090
- return JSON.stringify({ pid: process.pid, acquiredAt: (/* @__PURE__ */ new Date()).toISOString() });
2091
- }
2092
- function parseHolder(read) {
2093
- if (read.kind !== "ok") {
2094
- return void 0;
2095
- }
2096
- let parsed;
2097
- try {
2098
- parsed = JSON.parse(read.content);
2099
- } catch {
2100
- return void 0;
2101
- }
2102
- if (typeof parsed !== "object" || parsed === null) {
2103
- return void 0;
2104
- }
2105
- const record = parsed;
2106
- const holder = {};
2107
- if (typeof record.pid === "number") {
2108
- holder.pid = record.pid;
2109
- }
2110
- if (typeof record.acquiredAt === "string") {
2111
- holder.acquiredAt = record.acquiredAt;
2112
- }
2113
- return holder;
2114
- }
2115
- function makeWaitNotifier(path, fs, onWait, start) {
2116
- let holder;
2117
- let holderRead = false;
2118
- let lastEmit;
2119
- return async () => {
2120
- if (!holderRead) {
2121
- holderRead = true;
2122
- holder = parseHolder(await fs.readFileBounded(path, MAX_LOCK_PAYLOAD_BYTES));
2123
- }
2124
- const elapsedMs = Date.now() - start;
2125
- if (lastEmit !== void 0 && elapsedMs - lastEmit < WAIT_NOTICE_INTERVAL_MS) {
2126
- return;
2127
- }
2128
- lastEmit = elapsedMs;
2129
- onWait({ lockPath: path, elapsedMs, ...holder !== void 0 ? { holder } : {} });
2130
- };
2131
- }
2132
- async function acquireLock(path, fs, pollIntervalMs, deadline, notify) {
2133
- for (; ; ) {
2134
- if (await fs.createExclusive(path, lockPayload())) {
2135
- return;
2136
- }
2137
- if (notify !== void 0) {
2138
- await notify();
2139
- }
2140
- if (Date.now() >= deadline) {
2141
- throw new SdkError(
2142
- "LOCK_CONTENDED",
2143
- `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.`
2144
- );
2145
- }
2146
- const jitter = Math.random() * pollIntervalMs;
2147
- await sleep(pollIntervalMs + jitter);
2148
- }
2149
- }
2150
- async function withFileLock(path, fs, fn, options) {
2151
- const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
2152
- const acquireTimeoutMs = options.acquireTimeoutMs ?? DEFAULT_ACQUIRE_TIMEOUT_MS;
2153
- const start = Date.now();
2154
- const notify = options.onWait !== void 0 ? makeWaitNotifier(path, fs, options.onWait, start) : void 0;
2155
- await acquireLock(path, fs, pollIntervalMs, start + acquireTimeoutMs, notify);
2156
- try {
2157
- return await fn();
2158
- } finally {
2159
- await fs.deleteFile(path);
2160
- }
2161
- }
2162
- async function withLocaleWriteLock(cwd, locale, fs, fn, options = {}) {
2163
- return withFileLock(localeLockPath(cwd, locale), fs, fn, options);
2164
- }
2165
- async function withLockFileGuard(cwd, fs, fn, options = {}) {
2166
- return withFileLock(lockFileGuardPath(cwd), fs, fn, options);
2324
+ const pathFor = (locale) => path.resolve(cwd, expandPattern(pattern, safeSpelling(locale, style, config.sourceLocale)));
2325
+ const forward = buildForwardMap(config, pathFor);
2326
+ return {
2327
+ pathFor,
2328
+ localeFor: (absolutePath) => forward.get(path.resolve(cwd, absolutePath))
2329
+ };
2167
2330
  }
2168
-
2169
- // src/lock/lock-file.ts
2170
2331
  var LOCK_FILE_NAME = "verbatra.lock.json";
2171
2332
  var CURRENT_VERSION2 = 1;
2172
2333
  var EMPTY_LOCK = { version: CURRENT_VERSION2, locales: {} };
@@ -2249,6 +2410,87 @@ async function updateLockFileLocale(cwd, fs, locale, patch) {
2249
2410
  return next;
2250
2411
  });
2251
2412
  }
2413
+ async function cleanup(ops, tmp) {
2414
+ try {
2415
+ await ops.rm(tmp);
2416
+ } catch {
2417
+ }
2418
+ }
2419
+ function tempFileName2(path$1) {
2420
+ return path.join(path.dirname(path$1), `.${path.basename(path$1)}.tmp-${process.pid}-${Date.now()}-${crypto.randomUUID()}`);
2421
+ }
2422
+ async function atomicWriteFile(path$1, data, ops) {
2423
+ const directory = path.dirname(path$1);
2424
+ const tmp = tempFileName2(path$1);
2425
+ try {
2426
+ await ops.mkdir(directory);
2427
+ await ops.writeFile(tmp, data);
2428
+ await ops.fsyncFile(tmp);
2429
+ await ops.rename(tmp, path$1);
2430
+ } catch (error) {
2431
+ await cleanup(ops, tmp);
2432
+ throw error;
2433
+ }
2434
+ try {
2435
+ await ops.fsyncDir(directory);
2436
+ } catch {
2437
+ }
2438
+ }
2439
+ async function readUtf8(handle, size) {
2440
+ const buffer = Buffer.allocUnsafe(size);
2441
+ let offset = 0;
2442
+ while (offset < size) {
2443
+ const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
2444
+ if (bytesRead === 0) {
2445
+ break;
2446
+ }
2447
+ offset += bytesRead;
2448
+ }
2449
+ return buffer.toString("utf8", 0, offset);
2450
+ }
2451
+ async function nodeReadBounded(path, maxBytes) {
2452
+ const handle = await promises.open(path, "r");
2453
+ try {
2454
+ const info = await handle.stat();
2455
+ if (!info.isFile()) {
2456
+ return { kind: "not-a-file" };
2457
+ }
2458
+ if (info.size > maxBytes) {
2459
+ return { kind: "too-large" };
2460
+ }
2461
+ return { kind: "ok", content: await readUtf8(handle, info.size) };
2462
+ } finally {
2463
+ await handle.close();
2464
+ }
2465
+ }
2466
+ async function fsyncPath(path) {
2467
+ const handle = await promises.open(path, "r");
2468
+ try {
2469
+ await handle.sync();
2470
+ } finally {
2471
+ await handle.close();
2472
+ }
2473
+ }
2474
+ async function fsyncDirBestEffort(path) {
2475
+ try {
2476
+ await fsyncPath(path);
2477
+ } catch {
2478
+ }
2479
+ }
2480
+ var nodeOps = {
2481
+ mkdir: async (path) => {
2482
+ await promises.mkdir(path, { recursive: true });
2483
+ },
2484
+ writeFile: (path, data) => promises.writeFile(path, data, "utf8"),
2485
+ fsyncFile: (path) => fsyncPath(path),
2486
+ rename: (from, to) => promises.rename(from, to),
2487
+ fsyncDir: (path) => fsyncDirBestEffort(path),
2488
+ rm: (path) => promises.rm(path, { force: true })
2489
+ };
2490
+ var nodeAdapterFs = {
2491
+ readBounded: nodeReadBounded,
2492
+ writeFileAtomic: (path, data) => atomicWriteFile(path, data, nodeOps)
2493
+ };
2252
2494
  var VALID_EMPTY = { placeholders: [], isPlural: false, valid: true };
2253
2495
  var INVALID = { placeholders: [], isPlural: false, valid: false };
2254
2496
  function tokenOf(element) {
@@ -2718,85 +2960,12 @@ function buildCanHandle(extensions, sniff) {
2718
2960
  return sample === void 0 || sniff === void 0 || sniff(sample);
2719
2961
  };
2720
2962
  }
2721
- async function fsyncPath(path) {
2722
- const handle = await promises.open(path, "r");
2723
- try {
2724
- await handle.sync();
2725
- } finally {
2726
- await handle.close();
2727
- }
2728
- }
2729
- async function fsyncDirBestEffort(path) {
2730
- try {
2731
- await fsyncPath(path);
2732
- } catch {
2733
- }
2734
- }
2735
- var nodeOps = {
2736
- mkdir: async (path) => {
2737
- await promises.mkdir(path, { recursive: true });
2738
- },
2739
- writeFile: (path, data) => promises.writeFile(path, data, "utf8"),
2740
- fsyncFile: (path) => fsyncPath(path),
2741
- rename: (from, to) => promises.rename(from, to),
2742
- fsyncDir: (path) => fsyncDirBestEffort(path),
2743
- rm: (path) => promises.rm(path, { force: true })
2744
- };
2745
- async function cleanup(ops, tmp) {
2746
- try {
2747
- await ops.rm(tmp);
2748
- } catch {
2749
- }
2750
- }
2751
- function tempFileName2(path$1) {
2752
- return path.join(path.dirname(path$1), `.${path.basename(path$1)}.tmp-${process.pid}-${Date.now()}-${crypto.randomUUID()}`);
2753
- }
2754
- async function atomicWriteFile(path$1, data, ops = nodeOps) {
2755
- const directory = path.dirname(path$1);
2756
- const tmp = tempFileName2(path$1);
2757
- try {
2758
- await ops.mkdir(directory);
2759
- await ops.writeFile(tmp, data);
2760
- await ops.fsyncFile(tmp);
2761
- await ops.rename(tmp, path$1);
2762
- } catch (error) {
2763
- await cleanup(ops, tmp);
2764
- throw error;
2765
- }
2766
- try {
2767
- await ops.fsyncDir(directory);
2768
- } catch {
2769
- }
2770
- }
2771
2963
  function stripLeadingBom(content) {
2772
2964
  return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
2773
2965
  }
2774
- async function readBoundedUtf82(handle, size) {
2775
- const buffer = Buffer.allocUnsafe(size);
2776
- let offset = 0;
2777
- while (offset < size) {
2778
- const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
2779
- if (bytesRead === 0) {
2780
- break;
2781
- }
2782
- offset += bytesRead;
2783
- }
2784
- return stripLeadingBom(buffer.toString("utf8", 0, offset));
2785
- }
2786
- async function readBounded2(filePath) {
2787
- const handle = await promises.open(filePath, "r");
2788
- try {
2789
- const info = await handle.stat();
2790
- if (!info.isFile()) {
2791
- return { kind: "not-a-file" };
2792
- }
2793
- if (info.size > MAX_INPUT_BYTES) {
2794
- return { kind: "too-large" };
2795
- }
2796
- return { kind: "ok", content: await readBoundedUtf82(handle, info.size) };
2797
- } finally {
2798
- await handle.close();
2799
- }
2966
+ async function readBoundedFile(fs, filePath) {
2967
+ const outcome = await fs.readBounded(filePath, MAX_INPUT_BYTES);
2968
+ return outcome.kind === "ok" ? { kind: "ok", content: stripLeadingBom(outcome.content) } : outcome;
2800
2969
  }
2801
2970
  function outcomeToContent(outcome, notAFileMessage) {
2802
2971
  if (outcome.kind === "not-a-file") {
@@ -2807,8 +2976,8 @@ function outcomeToContent(outcome, notAFileMessage) {
2807
2976
  }
2808
2977
  return outcome.content;
2809
2978
  }
2810
- async function readFileContent(filePath) {
2811
- const outcome = await readBounded2(filePath);
2979
+ async function readFileContent(fs, filePath) {
2980
+ const outcome = await readBoundedFile(fs, filePath);
2812
2981
  return outcomeToContent(outcome, "The path is not a regular file.");
2813
2982
  }
2814
2983
  var BACKSLASH = "\\";
@@ -3019,7 +3188,8 @@ function createTreeFileAdapter(options) {
3019
3188
  buildWriteTree,
3020
3189
  comparePlaceholders,
3021
3190
  deriveDescriptions,
3022
- keyMode = "literal-leaf"
3191
+ keyMode = "literal-leaf",
3192
+ fs = nodeAdapterFs
3023
3193
  } = options;
3024
3194
  return {
3025
3195
  format,
@@ -3028,7 +3198,7 @@ function createTreeFileAdapter(options) {
3028
3198
  validateMessage: validateMessage ?? (() => true),
3029
3199
  ...comparePlaceholders !== void 0 ? { comparePlaceholders } : {},
3030
3200
  async read(filePath, locale) {
3031
- const content = await readFileContent(filePath);
3201
+ const content = await readFileContent(fs, filePath);
3032
3202
  const namespace = namespaceOf(filePath);
3033
3203
  const { entries, excludedLeafPaths } = toEntries(
3034
3204
  content,
@@ -3044,8 +3214,8 @@ function createTreeFileAdapter(options) {
3044
3214
  return { resource, invalidIcuKeys, excludedLeafPaths };
3045
3215
  },
3046
3216
  async write(resource, filePath) {
3047
- const tree = buildWriteTree ? await buildWriteTree(resource.entries, filePath) : unflattenEntries(resource.entries);
3048
- await atomicWriteFile(filePath, serialize2(tree));
3217
+ const tree = buildWriteTree ? await buildWriteTree(resource.entries, filePath, fs) : unflattenEntries(resource.entries);
3218
+ await fs.writeFileAtomic(filePath, serialize2(tree));
3049
3219
  }
3050
3220
  };
3051
3221
  }
@@ -3106,10 +3276,10 @@ function messagesFromEntries(entries) {
3106
3276
  }
3107
3277
  return out;
3108
3278
  }
3109
- async function readDestinationPairs(filePath) {
3279
+ async function readDestinationPairs(filePath, fs) {
3110
3280
  let content;
3111
3281
  try {
3112
- content = await readFileContent(filePath);
3282
+ content = await readFileContent(fs, filePath);
3113
3283
  } catch (error) {
3114
3284
  if (isEnoent(error)) {
3115
3285
  return null;
@@ -3118,9 +3288,9 @@ async function readDestinationPairs(filePath) {
3118
3288
  }
3119
3289
  return [...parseArbObject(content)];
3120
3290
  }
3121
- async function buildArbWriteTree(entries, filePath) {
3291
+ async function buildArbWriteTree(entries, filePath, fs) {
3122
3292
  const messages = messagesFromEntries(entries);
3123
- const pairs = await readDestinationPairs(filePath);
3293
+ const pairs = await readDestinationPairs(filePath, fs);
3124
3294
  const out = /* @__PURE__ */ new Map();
3125
3295
  const consumed = /* @__PURE__ */ new Set();
3126
3296
  for (const [key, value] of pairs ?? []) {
@@ -3142,8 +3312,9 @@ async function buildArbWriteTree(entries, filePath) {
3142
3312
  function parseArb(content) {
3143
3313
  return assertJsonRecord(stripArbMetadata(parseArbObject(content)));
3144
3314
  }
3145
- function createArbAdapter() {
3315
+ function createArbAdapter(fs = nodeAdapterFs) {
3146
3316
  return createTreeFileAdapter({
3317
+ fs,
3147
3318
  format: "arb",
3148
3319
  extensions: [".arb"],
3149
3320
  sniff: sniffJsonObject,
@@ -3167,6 +3338,38 @@ function createJsonFileAdapter(options) {
3167
3338
  serialize: serializeJsonTree
3168
3339
  });
3169
3340
  }
3341
+ var SINGLE_BRACE_PATTERN = /(?<!\{)\{\s*([A-Za-z_][\w$-]*|\d+)\s*\}(?!\})/g;
3342
+ function extractSingleBraceTokens(value) {
3343
+ return scanTokens(value, SINGLE_BRACE_PATTERN, (match) => {
3344
+ const key = match[1];
3345
+ return key !== void 0 ? `{${key}}` : void 0;
3346
+ });
3347
+ }
3348
+ function fabricatedTokens(sourceValue, targetValue) {
3349
+ const known = new Set(extractSingleBraceTokens(sourceValue));
3350
+ const invented = /* @__PURE__ */ new Set();
3351
+ for (const token of extractSingleBraceTokens(targetValue)) {
3352
+ if (!known.has(token)) {
3353
+ invented.add(token);
3354
+ }
3355
+ }
3356
+ return [...invented].sort();
3357
+ }
3358
+ function createSingleBraceFabricationComparator(extract) {
3359
+ return (sourceValue, targetValue) => {
3360
+ const format = checkPlaceholders(extract(sourceValue), extract(targetValue));
3361
+ const invented = fabricatedTokens(sourceValue, targetValue);
3362
+ if (invented.length === 0) {
3363
+ return format;
3364
+ }
3365
+ return {
3366
+ matches: false,
3367
+ missing: format.missing,
3368
+ extra: [...format.extra, ...invented].sort(),
3369
+ reordered: false
3370
+ };
3371
+ };
3372
+ }
3170
3373
  var DOUBLE_BRACE_PATTERN = /\{\{[^{}]*\}\}/g;
3171
3374
  var I18NEXT_PATTERN = /\{\{[^{}]*\}\}|\$t\([^()]*\)/g;
3172
3375
  function extractDoubleBracePlaceholders(value) {
@@ -3192,18 +3395,21 @@ function pluralBaseKey(key) {
3192
3395
  function makePluralKey(baseKey, category) {
3193
3396
  return `${baseKey}_${category}`;
3194
3397
  }
3195
- function createI18nextJsonAdapter() {
3398
+ function createI18nextJsonAdapter(fs = nodeAdapterFs) {
3196
3399
  return createJsonFileAdapter({
3400
+ fs,
3197
3401
  format: "i18next-json",
3198
3402
  extractPlaceholders: extractI18nextPlaceholders,
3199
3403
  deriveEntry: (key, value) => ({
3200
3404
  placeholders: extractI18nextPlaceholders(value),
3201
3405
  isPlural: isPluralKey(key)
3202
- })
3406
+ }),
3407
+ comparePlaceholders: createSingleBraceFabricationComparator(extractI18nextPlaceholders)
3203
3408
  });
3204
3409
  }
3205
- function createNextIntlJsonAdapter() {
3410
+ function createNextIntlJsonAdapter(fs = nodeAdapterFs) {
3206
3411
  return createJsonFileAdapter({
3412
+ fs,
3207
3413
  format: "next-intl-json",
3208
3414
  extractPlaceholders: icuPlaceholders,
3209
3415
  deriveEntry: icuDeriveEntry,
@@ -3244,10 +3450,10 @@ function assertNotMixed(tree) {
3244
3450
  }
3245
3451
  assertNoDottedNestedKey(tree);
3246
3452
  }
3247
- async function detectStyle(filePath) {
3453
+ async function detectStyle(filePath, fs) {
3248
3454
  let parsed;
3249
3455
  try {
3250
- const outcome = await readBounded2(filePath);
3456
+ const outcome = await readBoundedFile(fs, filePath);
3251
3457
  if (outcome.kind !== "ok") {
3252
3458
  return "nested";
3253
3459
  }
@@ -3272,18 +3478,20 @@ function buildFlatTree(entries) {
3272
3478
  }
3273
3479
  return out;
3274
3480
  }
3275
- async function buildNgxWriteTree(entries, filePath) {
3276
- const style = await detectStyle(filePath);
3481
+ async function buildNgxWriteTree(entries, filePath, fs) {
3482
+ const style = await detectStyle(filePath, fs);
3277
3483
  return style === "flat" ? buildFlatTree(entries) : unflattenEntries(entries);
3278
3484
  }
3279
- function createNgxTranslateJsonAdapter() {
3485
+ function createNgxTranslateJsonAdapter(fs = nodeAdapterFs) {
3280
3486
  return createJsonFileAdapter({
3487
+ fs,
3281
3488
  format: "ngx-translate-json",
3282
3489
  extractPlaceholders: extractDoubleBracePlaceholders,
3283
3490
  deriveEntry: (_key, value) => ({
3284
3491
  placeholders: extractDoubleBracePlaceholders(value),
3285
3492
  isPlural: false
3286
3493
  }),
3494
+ comparePlaceholders: createSingleBraceFabricationComparator(extractDoubleBracePlaceholders),
3287
3495
  validateTree: assertNotMixed,
3288
3496
  buildWriteTree: buildNgxWriteTree,
3289
3497
  keyMode: "path-notation"
@@ -3305,7 +3513,8 @@ function createFlatFileAdapter(options) {
3305
3513
  serializeEntries,
3306
3514
  extractPlaceholders,
3307
3515
  validateMessage,
3308
- computeInvalidIcuKeys
3516
+ computeInvalidIcuKeys,
3517
+ fs = nodeAdapterFs
3309
3518
  } = options;
3310
3519
  return {
3311
3520
  format,
@@ -3313,7 +3522,7 @@ function createFlatFileAdapter(options) {
3313
3522
  extractPlaceholders,
3314
3523
  validateMessage: validateMessage ?? (() => true),
3315
3524
  async read(filePath, locale) {
3316
- const content = await readFileContent(filePath);
3525
+ const content = await readFileContent(fs, filePath);
3317
3526
  const namespace = namespaceOf(filePath);
3318
3527
  const entries = toEntries2(content, namespace, parseEntries);
3319
3528
  const resource = { locale, namespace, format, entries };
@@ -3321,8 +3530,8 @@ function createFlatFileAdapter(options) {
3321
3530
  return { resource, invalidIcuKeys, excludedLeafPaths: [] };
3322
3531
  },
3323
3532
  async write(resource, filePath) {
3324
- const data = await serializeEntries(resource.entries, filePath);
3325
- await atomicWriteFile(filePath, data);
3533
+ const data = await serializeEntries(resource.entries, filePath, fs);
3534
+ await fs.writeFileAtomic(filePath, data);
3326
3535
  }
3327
3536
  };
3328
3537
  }
@@ -3330,13 +3539,13 @@ var SUBMESSAGE_TYPES = /* @__PURE__ */ new Set(["plural", "select", "selectordin
3330
3539
  var ARGUMENT_NAME = /^(?:\d+|[A-Za-z_$][\w$-]*)$/;
3331
3540
  function matchingBraces(value) {
3332
3541
  const close = new Int32Array(value.length).fill(-1);
3333
- const open3 = [];
3542
+ const open22 = [];
3334
3543
  for (let i = 0; i < value.length; i += 1) {
3335
3544
  const char = value[i];
3336
3545
  if (char === "{") {
3337
- open3.push(i);
3546
+ open22.push(i);
3338
3547
  } else if (char === "}") {
3339
- const start = open3.pop();
3548
+ const start = open22.pop();
3340
3549
  if (start !== void 0) {
3341
3550
  close[start] = i;
3342
3551
  }
@@ -3356,13 +3565,13 @@ function findNameEnd(value, from, limit) {
3356
3565
  }
3357
3566
  return { comma: -1, hasBrace: false };
3358
3567
  }
3359
- function parseArgumentAt(value, open3, close) {
3360
- const { comma, hasBrace } = findNameEnd(value, open3 + 1, close);
3568
+ function parseArgumentAt(value, open22, close) {
3569
+ const { comma, hasBrace } = findNameEnd(value, open22 + 1, close);
3361
3570
  if (hasBrace) {
3362
3571
  return null;
3363
3572
  }
3364
3573
  const nameEnd = comma === -1 ? close : comma;
3365
- const name = value.slice(open3 + 1, nameEnd).trim();
3574
+ const name = value.slice(open22 + 1, nameEnd).trim();
3366
3575
  if (!ARGUMENT_NAME.test(name)) {
3367
3576
  return null;
3368
3577
  }
@@ -3629,10 +3838,10 @@ function escapeString(input, isKey) {
3629
3838
  function formatEntry(key, value) {
3630
3839
  return `${escapeString(key, true)}=${escapeString(value, false)}`;
3631
3840
  }
3632
- async function readStructure(filePath) {
3841
+ async function readStructure(filePath, fs) {
3633
3842
  let outcome;
3634
3843
  try {
3635
- outcome = await readBounded2(filePath);
3844
+ outcome = await readBoundedFile(fs, filePath);
3636
3845
  } catch (error) {
3637
3846
  if (isEnoent(error)) {
3638
3847
  return { items: [], terminator: "\n" };
@@ -3645,8 +3854,8 @@ async function readStructure(filePath) {
3645
3854
  terminator: detectLineTerminator(content)
3646
3855
  };
3647
3856
  }
3648
- async function serializePropertiesEntries(entries, filePath) {
3649
- const { items, terminator } = await readStructure(filePath);
3857
+ async function serializePropertiesEntries(entries, filePath, fs) {
3858
+ const { items, terminator } = await readStructure(filePath, fs);
3650
3859
  const emitted = /* @__PURE__ */ new Set();
3651
3860
  const lines = [];
3652
3861
  for (const item of items) {
@@ -3667,8 +3876,9 @@ async function serializePropertiesEntries(entries, filePath) {
3667
3876
  }
3668
3877
  return lines.length === 0 ? "" : `${lines.join(terminator)}${terminator}`;
3669
3878
  }
3670
- function createPropertiesAdapter() {
3879
+ function createPropertiesAdapter(fs = nodeAdapterFs) {
3671
3880
  return createFlatFileAdapter({
3881
+ fs,
3672
3882
  format: "properties",
3673
3883
  extensions: [".properties"],
3674
3884
  parseEntries: parsePropertiesEntries,
@@ -3725,18 +3935,15 @@ var AdapterRegistry = class {
3725
3935
  return this.resolveByDetection(filePath, options.sample);
3726
3936
  }
3727
3937
  };
3728
- var PLACEHOLDER_PATTERN = /(?<!\{)\{\s*([A-Za-z_][\w$-]*|\d+)\s*\}(?!\})/g;
3729
3938
  function extractVueI18nPlaceholders(value) {
3730
- return scanTokens(value, PLACEHOLDER_PATTERN, (match) => {
3731
- const key = match[1];
3732
- return key !== void 0 ? `{${key}}` : void 0;
3733
- });
3939
+ return extractSingleBraceTokens(value);
3734
3940
  }
3735
3941
  function isPluralValue(value) {
3736
3942
  return value.includes("|");
3737
3943
  }
3738
- function createVueI18nJsonAdapter() {
3944
+ function createVueI18nJsonAdapter(fs = nodeAdapterFs) {
3739
3945
  return createJsonFileAdapter({
3946
+ fs,
3740
3947
  format: "vue-i18n-json",
3741
3948
  extractPlaceholders: extractVueI18nPlaceholders,
3742
3949
  deriveEntry: (_key, value) => ({
@@ -3883,10 +4090,10 @@ function parseXliffEntries(content, namespace) {
3883
4090
  }
3884
4091
  return out;
3885
4092
  }
3886
- async function readDestination(filePath) {
4093
+ async function readDestination(filePath, fs) {
3887
4094
  let outcome;
3888
4095
  try {
3889
- outcome = await readBounded2(filePath);
4096
+ outcome = await readBoundedFile(fs, filePath);
3890
4097
  } catch {
3891
4098
  throw new AdapterError("INVALID_STRUCTURE", "The destination XLIFF file does not exist.");
3892
4099
  }
@@ -3970,8 +4177,8 @@ function setTargetValue(doc, parser, element, value) {
3970
4177
  element.appendChild(doc.importNode(node, true));
3971
4178
  }
3972
4179
  }
3973
- async function serializeXliffEntries(entries, filePath) {
3974
- const { doc, root } = parseXml(await readDestination(filePath));
4180
+ async function serializeXliffEntries(entries, filePath, fs) {
4181
+ const { doc, root } = parseXml(await readDestination(filePath, fs));
3975
4182
  const parser = new xmldom.DOMParser({ onError: onFatal });
3976
4183
  for (const unit of walkUnits(root)) {
3977
4184
  const entry = entries.get(unit.key);
@@ -3989,8 +4196,9 @@ function sniffXliff(sample) {
3989
4196
  const head = sample.trimStart();
3990
4197
  return head.startsWith("<xliff") || head.startsWith("<?xml");
3991
4198
  }
3992
- function createXliffAdapter() {
4199
+ function createXliffAdapter(fs = nodeAdapterFs) {
3993
4200
  return createFlatFileAdapter({
4201
+ fs,
3994
4202
  format: "xliff",
3995
4203
  extensions: [".xlf", ".xliff"],
3996
4204
  sniff: sniffXliff,
@@ -4028,8 +4236,9 @@ function parseYamlObject(content) {
4028
4236
  assertWithinDepth(parsed, MAX_DEPTH);
4029
4237
  return assertJsonRecord(normalizeYamlTree(parsed));
4030
4238
  }
4031
- function createYamlAdapter() {
4239
+ function createYamlAdapter(fs = nodeAdapterFs) {
4032
4240
  return createTreeFileAdapter({
4241
+ fs,
4033
4242
  format: "yaml",
4034
4243
  extensions: [".yml", ".yaml"],
4035
4244
  parse: parseYamlObject,
@@ -4038,16 +4247,41 @@ function createYamlAdapter() {
4038
4247
  deriveEntry: (_key, value) => ({
4039
4248
  placeholders: extractDoubleBracePlaceholders(value),
4040
4249
  isPlural: false
4041
- })
4250
+ }),
4251
+ comparePlaceholders: createSingleBraceFabricationComparator(extractDoubleBracePlaceholders)
4252
+ });
4253
+ }
4254
+ function createDefaultRegistry(fs = nodeAdapterFs) {
4255
+ return new AdapterRegistry().register(createI18nextJsonAdapter(fs)).register(createVueI18nJsonAdapter(fs)).register(createNextIntlJsonAdapter(fs)).register(createNgxTranslateJsonAdapter(fs)).register(createXliffAdapter(fs)).register(createYamlAdapter(fs)).register(createArbAdapter(fs)).register(createPropertiesAdapter(fs));
4256
+ }
4257
+ function enoent(path) {
4258
+ return Object.assign(new Error(`ENOENT: no such file or directory, open '${path}'`), {
4259
+ code: "ENOENT"
4042
4260
  });
4043
4261
  }
4044
- function createDefaultRegistry() {
4045
- return new AdapterRegistry().register(createI18nextJsonAdapter()).register(createVueI18nJsonAdapter()).register(createNextIntlJsonAdapter()).register(createNgxTranslateJsonAdapter()).register(createXliffAdapter()).register(createYamlAdapter()).register(createArbAdapter()).register(createPropertiesAdapter());
4262
+ function toAdapterFs(fs) {
4263
+ return {
4264
+ async readBounded(path, maxBytes) {
4265
+ const outcome = await fs.readFileBounded(path, maxBytes);
4266
+ if (outcome.kind === "missing") {
4267
+ throw enoent(path);
4268
+ }
4269
+ if (outcome.kind === "too-large") {
4270
+ return { kind: "too-large" };
4271
+ }
4272
+ return { kind: "ok", content: outcome.content };
4273
+ },
4274
+ async writeFileAtomic(path$1, data) {
4275
+ await fs.mkdir?.(path.dirname(path$1));
4276
+ await fs.writeFile(path$1, data);
4277
+ }
4278
+ };
4046
4279
  }
4047
4280
 
4048
4281
  // src/selection/select-adapter.ts
4049
- function selectAdapter(format, registry = createDefaultRegistry()) {
4050
- const resolution = registry.resolve("", { format });
4282
+ function selectAdapter(format, registry, fs) {
4283
+ const resolved = registry ?? createDefaultRegistry(fs === void 0 ? void 0 : toAdapterFs(fs));
4284
+ const resolution = resolved.resolve("", { format });
4051
4285
  if (resolution.status === "resolved") {
4052
4286
  return resolution.adapter;
4053
4287
  }
@@ -4058,12 +4292,25 @@ function selectAdapter(format, registry = createDefaultRegistry()) {
4058
4292
  }
4059
4293
 
4060
4294
  // src/flow/read-target.ts
4295
+ function attributeTargetRead(error, locale, path) {
4296
+ if (!(error instanceof AdapterError)) {
4297
+ return error;
4298
+ }
4299
+ return new AdapterError(
4300
+ error.code,
4301
+ `The ${locale} locale file at ${path} could not be read: ${error.message}`
4302
+ );
4303
+ }
4061
4304
  async function readTargetResource(input) {
4062
4305
  const path = input.resolver.pathFor(input.locale);
4063
4306
  if (!await input.fs.fileExists(path)) {
4064
4307
  return { locale: input.locale, namespace: "", format: input.format, entries: /* @__PURE__ */ new Map() };
4065
4308
  }
4066
- return (await input.adapter.read(path, input.locale)).resource;
4309
+ try {
4310
+ return (await input.adapter.read(path, input.locale)).resource;
4311
+ } catch (error) {
4312
+ throw attributeTargetRead(error, input.locale, path);
4313
+ }
4067
4314
  }
4068
4315
 
4069
4316
  // src/flow/select-locales.ts
@@ -4121,7 +4368,7 @@ async function diffLocales(input, deps = {}) {
4121
4368
  const config = input.config;
4122
4369
  const cwd = input.cwd ?? process.cwd();
4123
4370
  const fs = deps.fs ?? defaultFs;
4124
- const adapter = selectAdapter(config.format, deps.adapterRegistry);
4371
+ const adapter = selectAdapter(config.format, deps.adapterRegistry, deps.fs);
4125
4372
  const resolver = createLocalePathResolver(cwd, config);
4126
4373
  const source = await readSourceResource(config, resolver, fs, adapter);
4127
4374
  const lock = await readLockFile(lockFilePath(cwd), fs);
@@ -4172,6 +4419,133 @@ async function diff(input, deps = {}) {
4172
4419
  return { hasPendingChanges: locales.some((entry) => entry.hasPendingChanges), locales };
4173
4420
  }
4174
4421
 
4422
+ // src/flow/doctor.ts
4423
+ var CHECK_TITLES = {
4424
+ config: "Configuration",
4425
+ "format-adapter": "Format adapter",
4426
+ provider: "Provider",
4427
+ "api-key": "API key environment variable",
4428
+ "source-file": "Source locale file"
4429
+ };
4430
+ var CONFIG_DEPENDENT_IDS = [
4431
+ "format-adapter",
4432
+ "provider",
4433
+ "api-key",
4434
+ "source-file"
4435
+ ];
4436
+ var SKIPPED_DETAIL = "Not checked: the configuration could not be loaded.";
4437
+ function check2(id, status, detail) {
4438
+ return { id, title: CHECK_TITLES[id], status, detail };
4439
+ }
4440
+ function verdict(id, passed, detail) {
4441
+ return check2(id, passed ? "pass" : "fail", detail);
4442
+ }
4443
+ function toResult(checks) {
4444
+ return { ok: checks.every((entry) => entry.status !== "fail"), checks };
4445
+ }
4446
+ function loadOptionsFor(input, deps) {
4447
+ return {
4448
+ ...input.cwd !== void 0 ? { cwd: input.cwd } : {},
4449
+ ...input.configPath !== void 0 ? { configPath: input.configPath } : {},
4450
+ ...deps.fs !== void 0 ? { fs: deps.fs } : {}
4451
+ };
4452
+ }
4453
+ function isMissingExplicitConfig(error, input) {
4454
+ return input.configPath !== void 0 && error instanceof SdkError && error.code === "CONFIG_NOT_FOUND";
4455
+ }
4456
+ async function loadForDoctor(input, deps) {
4457
+ const load = deps.loadConfig ?? loadConfigWithMeta;
4458
+ try {
4459
+ return { kind: "loaded", loaded: await load(loadOptionsFor(input, deps)) };
4460
+ } catch (error) {
4461
+ if (isMissingExplicitConfig(error, input)) {
4462
+ throw error;
4463
+ }
4464
+ return { kind: "failed", detail: errorMessage(error) };
4465
+ }
4466
+ }
4467
+ function configDetail(source) {
4468
+ return source.kind === "override" ? "Validated the config supplied in memory." : `Loaded ${source.filepath}.`;
4469
+ }
4470
+ function resolveAdapter(config, deps) {
4471
+ try {
4472
+ const adapter = selectAdapter(config.format, deps.adapterRegistry, deps.fs);
4473
+ return { kind: "resolved", adapter };
4474
+ } catch (error) {
4475
+ return { kind: "failed", detail: errorMessage(error) };
4476
+ }
4477
+ }
4478
+ function checkAdapter(config, outcome) {
4479
+ return outcome.kind === "resolved" ? verdict("format-adapter", true, `Format "${config.format}" resolves to an adapter.`) : verdict("format-adapter", false, outcome.detail);
4480
+ }
4481
+ function checkProvider(provider) {
4482
+ return hasProviderFactory(provider.id) ? verdict("provider", true, `Provider "${provider.id}" resolves to a factory.`) : verdict(
4483
+ "provider",
4484
+ false,
4485
+ `No factory is registered for provider "${provider.id}". Supported providers: ${PROVIDER_IDS.join(", ")}.`
4486
+ );
4487
+ }
4488
+ function isEnvVarSet(name) {
4489
+ const value = process.env[name];
4490
+ return value !== void 0 && value.length > 0;
4491
+ }
4492
+ function envVarVerdict(name) {
4493
+ return isEnvVarSet(name) ? verdict("api-key", true, `${name} is set.`) : verdict("api-key", false, `The ${name} environment variable is not set.`);
4494
+ }
4495
+ function checkOpenAiCompatibleKey(apiKeyEnvVar) {
4496
+ if (apiKeyEnvVar === void 0) {
4497
+ return verdict(
4498
+ "api-key",
4499
+ true,
4500
+ `The openai-compatible provider needs no API key. Set ${OPENAI_COMPATIBLE_ENV_VAR} only if your server requires one, or name your own variable with provider.options.apiKeyEnvVar.`
4501
+ );
4502
+ }
4503
+ return envVarVerdict(apiKeyEnvVar);
4504
+ }
4505
+ function checkApiKey(provider) {
4506
+ return provider.id === "openai-compatible" ? checkOpenAiCompatibleKey(provider.options.apiKeyEnvVar) : envVarVerdict(PROVIDER_ENV[provider.id]);
4507
+ }
4508
+ var NOT_PARSED_DETAIL = "Its contents were not checked, because the configured format resolves to no adapter.";
4509
+ async function existenceOnlyVerdict(sourcePath, fs) {
4510
+ return await fs.fileExists(sourcePath) ? verdict("source-file", true, `Found ${sourcePath}. ${NOT_PARSED_DETAIL}`) : verdict("source-file", false, `The source locale file was not found at ${sourcePath}.`);
4511
+ }
4512
+ async function parseSourceVerdict(config, resolver, fs, adapter, sourcePath) {
4513
+ const { resource } = await readSourceResource(config, resolver, fs, adapter);
4514
+ const count = resource.entries.size;
4515
+ return verdict(
4516
+ "source-file",
4517
+ true,
4518
+ `Read ${sourcePath} (${count} translatable ${count === 1 ? "key" : "keys"}).`
4519
+ );
4520
+ }
4521
+ async function checkSourceFile(config, cwd, fs, outcome) {
4522
+ try {
4523
+ const resolver = createLocalePathResolver(cwd, config);
4524
+ const sourcePath = resolver.pathFor(config.sourceLocale);
4525
+ return outcome.kind === "resolved" ? await parseSourceVerdict(config, resolver, fs, outcome.adapter, sourcePath) : await existenceOnlyVerdict(sourcePath, fs);
4526
+ } catch (error) {
4527
+ return verdict("source-file", false, errorMessage(error));
4528
+ }
4529
+ }
4530
+ async function doctor(input = {}, deps = {}) {
4531
+ const outcome = await loadForDoctor(input, deps);
4532
+ if (outcome.kind === "failed") {
4533
+ return toResult([
4534
+ verdict("config", false, outcome.detail),
4535
+ ...CONFIG_DEPENDENT_IDS.map((id) => check2(id, "skipped", SKIPPED_DETAIL))
4536
+ ]);
4537
+ }
4538
+ const { config, source } = outcome.loaded;
4539
+ const adapter = resolveAdapter(config, deps);
4540
+ return toResult([
4541
+ verdict("config", true, configDetail(source)),
4542
+ checkAdapter(config, adapter),
4543
+ checkProvider(config.provider),
4544
+ checkApiKey(config.provider),
4545
+ await checkSourceFile(config, input.cwd ?? process.cwd(), deps.fs ?? defaultFs, adapter)
4546
+ ]);
4547
+ }
4548
+
4175
4549
  // src/cache/fingerprint.ts
4176
4550
  function fingerprintModel(provider) {
4177
4551
  const options = provider.options;
@@ -4208,13 +4582,51 @@ function gateCandidateValue(sourceEntry, candidateValue, adapter) {
4208
4582
  }
4209
4583
  return { accepted: true };
4210
4584
  }
4585
+ var REMEDY_BY_CODE = {
4586
+ EACCES: "Check the write permissions on the containing directory, then run again.",
4587
+ EPERM: "Check the write permissions on the containing directory, then run again.",
4588
+ EROFS: "That file system is mounted read-only, so nothing can be written there.",
4589
+ ENOSPC: "The device it lives on has no space left.",
4590
+ ENOENT: "Check that the containing directory exists and is reachable, then run again.",
4591
+ EISDIR: "A directory already exists at that path, so no file can take its place."
4592
+ };
4593
+ var DEFAULT_REMEDY = "Check that the containing directory exists and is writable, then run again.";
4594
+ function displayPath(targetPath, cwd) {
4595
+ const relativePath = path.relative(cwd, targetPath);
4596
+ if (relativePath.length === 0 || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
4597
+ return targetPath;
4598
+ }
4599
+ return relativePath.split(path.sep).join("/");
4600
+ }
4601
+ function fsErrorCode(error) {
4602
+ if (error instanceof Error && "code" in error && typeof error.code === "string") {
4603
+ return error.code;
4604
+ }
4605
+ return void 0;
4606
+ }
4607
+ function targetUnwritableMessage(targetPath, cwd, error) {
4608
+ const code = fsErrorCode(error);
4609
+ const remedy = (code === void 0 ? void 0 : REMEDY_BY_CODE[code]) ?? DEFAULT_REMEDY;
4610
+ const detail = code === void 0 ? "" : ` (${code})`;
4611
+ return `Could not write the locale file ${displayPath(targetPath, cwd)}${detail}. ${remedy}`;
4612
+ }
4613
+ async function writeTargetResource(adapter, resource, targetPath, cwd) {
4614
+ try {
4615
+ await adapter.write(resource, targetPath);
4616
+ } catch (error) {
4617
+ if (error instanceof AdapterError || error instanceof SdkError) {
4618
+ throw error;
4619
+ }
4620
+ throw new SdkError("TARGET_UNWRITABLE", targetUnwritableMessage(targetPath, cwd, error));
4621
+ }
4622
+ }
4211
4623
 
4212
4624
  // src/flow/edit-entry.ts
4213
4625
  async function editEntry(input, deps = {}) {
4214
4626
  const config = input.config;
4215
4627
  const cwd = input.cwd ?? process.cwd();
4216
4628
  const fs = deps.fs ?? defaultFs;
4217
- const adapter = selectAdapter(config.format, deps.adapterRegistry);
4629
+ const adapter = selectAdapter(config.format, deps.adapterRegistry, deps.fs);
4218
4630
  const [locale] = selectLocales(config, [input.locale]);
4219
4631
  if (locale === void 0) {
4220
4632
  throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
@@ -4236,9 +4648,11 @@ async function editEntry(input, deps = {}) {
4236
4648
  const merged = new Map(target.entries);
4237
4649
  merged.set(input.key, { ...sourceEntry, value: input.value, namespace: target.namespace });
4238
4650
  const path = createLocalePathResolver(cwd, config).pathFor(locale);
4239
- await adapter.write(
4651
+ await writeTargetResource(
4652
+ adapter,
4240
4653
  { locale, namespace: target.namespace, format: config.format, entries: merged },
4241
- path
4654
+ path,
4655
+ cwd
4242
4656
  );
4243
4657
  await updateLockFileLocale(cwd, fs, locale, {
4244
4658
  mode: "merge",
@@ -4289,7 +4703,7 @@ async function keyIntegrity(input, deps = {}) {
4289
4703
  const config = input.config;
4290
4704
  const cwd = input.cwd ?? process.cwd();
4291
4705
  const fs = deps.fs ?? defaultFs;
4292
- const adapter = selectAdapter(config.format, deps.adapterRegistry);
4706
+ const adapter = selectAdapter(config.format, deps.adapterRegistry, deps.fs);
4293
4707
  const resolver = createLocalePathResolver(cwd, config);
4294
4708
  const source = await readSourceResource(config, resolver, fs, adapter);
4295
4709
  const lock = await readLockFile(lockFilePath(cwd), fs);
@@ -4317,7 +4731,7 @@ async function keyValue(input, deps = {}) {
4317
4731
  const config = input.config;
4318
4732
  const cwd = input.cwd ?? process.cwd();
4319
4733
  const fs = deps.fs ?? defaultFs;
4320
- const adapter = selectAdapter(config.format, deps.adapterRegistry);
4734
+ const adapter = selectAdapter(config.format, deps.adapterRegistry, deps.fs);
4321
4735
  const [locale] = selectLocales(config, [input.locale]);
4322
4736
  if (locale === void 0) {
4323
4737
  throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
@@ -4342,7 +4756,7 @@ async function keyValue(input, deps = {}) {
4342
4756
  async function readLocaleFileSnapshot(input, deps = {}) {
4343
4757
  const cwd = input.cwd ?? process.cwd();
4344
4758
  const fs = deps.fs ?? defaultFs;
4345
- const adapter = selectAdapter(input.config.format, deps.adapterRegistry);
4759
+ const adapter = selectAdapter(input.config.format, deps.adapterRegistry, deps.fs);
4346
4760
  const resource = await readTarget(cwd, input.config, adapter, fs, input.locale);
4347
4761
  const hashes = /* @__PURE__ */ new Map();
4348
4762
  for (const [key, entry] of resource.entries) {
@@ -4391,7 +4805,7 @@ async function lockState(input, deps = {}) {
4391
4805
  return { exists: false };
4392
4806
  }
4393
4807
  const lock = await readLockFile(path, fs);
4394
- const adapter = selectAdapter(config.format, deps.adapterRegistry);
4808
+ const adapter = selectAdapter(config.format, deps.adapterRegistry, deps.fs);
4395
4809
  const source = await readSource(config, cwd, fs, adapter);
4396
4810
  const localeStates = await Promise.all(
4397
4811
  locales.map(async (locale) => {
@@ -4435,7 +4849,7 @@ async function retranslateEntry(input, deps = {}) {
4435
4849
  const config = input.config;
4436
4850
  const cwd = input.cwd ?? process.cwd();
4437
4851
  const fs = deps.fs ?? defaultFs;
4438
- const adapter = selectAdapter(config.format, deps.adapterRegistry);
4852
+ const adapter = selectAdapter(config.format, deps.adapterRegistry, deps.fs);
4439
4853
  const [locale] = selectLocales(config, [input.locale]);
4440
4854
  if (locale === void 0) {
4441
4855
  throw new SdkError("UNKNOWN_LOCALE", `Locale "${input.locale}" could not be resolved.`);
@@ -4477,9 +4891,11 @@ async function retranslateEntry(input, deps = {}) {
4477
4891
  const merged = new Map(target.entries);
4478
4892
  merged.set(input.key, { ...sourceEntry, value, namespace: target.namespace });
4479
4893
  const path = createLocalePathResolver(cwd, config).pathFor(locale);
4480
- await adapter.write(
4894
+ await writeTargetResource(
4895
+ adapter,
4481
4896
  { locale, namespace: target.namespace, format: config.format, entries: merged },
4482
- path
4897
+ path,
4898
+ cwd
4483
4899
  );
4484
4900
  await updateLockFileLocale(cwd, fs, locale, {
4485
4901
  mode: "merge",
@@ -4569,7 +4985,12 @@ function fromParsed(data) {
4569
4985
  };
4570
4986
  }
4571
4987
  async function readRunStatusFile(path, fs) {
4572
- const read = await fs.readFileBounded(path, MAX_RUN_STATUS_FILE_BYTES);
4988
+ let read;
4989
+ try {
4990
+ read = await fs.readFileBounded(path, MAX_RUN_STATUS_FILE_BYTES);
4991
+ } catch {
4992
+ return void 0;
4993
+ }
4573
4994
  if (read.kind !== "ok") {
4574
4995
  return void 0;
4575
4996
  }
@@ -5185,14 +5606,16 @@ async function runLocale(params) {
5185
5606
  generated: generation.accepted.length
5186
5607
  });
5187
5608
  if (writeNeeded) {
5188
- await params.adapter.write(
5609
+ await writeTargetResource(
5610
+ params.adapter,
5189
5611
  {
5190
5612
  locale: params.targetLocale,
5191
5613
  namespace: target.namespace,
5192
5614
  format: params.format,
5193
5615
  entries: merged
5194
5616
  },
5195
- path
5617
+ path,
5618
+ params.cwd
5196
5619
  );
5197
5620
  }
5198
5621
  const pluralNotices = params.generatePlurals ? pluralNoticeFor(params, merged) : sdkNotices;
@@ -5611,6 +6034,7 @@ async function translate2(input, deps = {}) {
5611
6034
  const config = input.config;
5612
6035
  const cwd = input.cwd ?? process.cwd();
5613
6036
  const dryRun = input.dryRun ?? false;
6037
+ const targetLocales = selectLocales(config, input.locales);
5614
6038
  const concurrency = resolveRunConcurrency(input.concurrency, dryRun, config);
5615
6039
  const prune = input.prune ?? config.prune ?? false;
5616
6040
  const generatePlurals = input.generatePlurals ?? config.generatePlurals ?? false;
@@ -5621,7 +6045,7 @@ async function translate2(input, deps = {}) {
5621
6045
  config.budgetBehavior ?? DEFAULT_BUDGET_BEHAVIOR
5622
6046
  );
5623
6047
  const resolver = createLocalePathResolver(cwd, config);
5624
- const adapter = selectAdapter(config.format, deps.adapterRegistry);
6048
+ const adapter = selectAdapter(config.format, deps.adapterRegistry, deps.fs);
5625
6049
  const provider = dryRun ? void 0 : selectProvider(config.provider, deps.createProvider);
5626
6050
  const source = await readSourceResource(config, resolver, fs, adapter);
5627
6051
  const cache = await createRunCacheState(input, config, cwd, dryRun, fs);
@@ -5642,7 +6066,7 @@ async function translate2(input, deps = {}) {
5642
6066
  ...input.onProgress !== void 0 ? { onProgress: input.onProgress } : {},
5643
6067
  ...input.lockAcquireTimeoutMs !== void 0 ? { lockAcquireTimeoutMs: input.lockAcquireTimeoutMs } : {}
5644
6068
  };
5645
- const summaries = dryRun ? await runAllLocalesDry(context, config.targetLocales, concurrency) : await runAllLocalesLive(context, config.targetLocales, concurrency);
6069
+ const summaries = dryRun ? await runAllLocalesDry(context, targetLocales, concurrency) : await runAllLocalesLive(context, targetLocales, concurrency);
5646
6070
  input.onProgress?.({ type: "run-finished", localesCompleted: summaries.length });
5647
6071
  const locales = withCacheNotices(summaries, cache);
5648
6072
  const { succeeded, partial, failed } = partition(locales);
@@ -6509,7 +6933,7 @@ async function exportWorkbook(input, deps = {}) {
6509
6933
  const config = input.config;
6510
6934
  const cwd = input.cwd ?? process.cwd();
6511
6935
  const fs = deps.fs ?? defaultFs;
6512
- const adapter = selectAdapter(config.format, deps.adapterRegistry);
6936
+ const adapter = selectAdapter(config.format, deps.adapterRegistry, deps.fs);
6513
6937
  const resolver = createLocalePathResolver(cwd, config);
6514
6938
  const source = await readSourceResource(config, resolver, fs, adapter);
6515
6939
  const lock = await readLockFile(lockFilePath(cwd), fs);
@@ -6860,14 +7284,16 @@ async function runSheet(ctx, sheet, lock) {
6860
7284
  const merged = mergeAccepted(target, accepted);
6861
7285
  if (accepted.size > 0) {
6862
7286
  const path = ctx.resolver.pathFor(sheet.locale);
6863
- await ctx.adapter.write(
7287
+ await writeTargetResource(
7288
+ ctx.adapter,
6864
7289
  {
6865
7290
  locale: sheet.locale,
6866
7291
  namespace: target.namespace,
6867
7292
  format: ctx.config.format,
6868
7293
  entries: merged
6869
7294
  },
6870
- path
7295
+ path,
7296
+ ctx.cwd
6871
7297
  );
6872
7298
  }
6873
7299
  return {
@@ -6881,7 +7307,7 @@ async function importWorkbook(input, deps = {}) {
6881
7307
  const cwd = input.cwd ?? process.cwd();
6882
7308
  const dryRun = input.dryRun ?? false;
6883
7309
  const fs = deps.fs ?? defaultFs;
6884
- const adapter = selectAdapter(config.format, deps.adapterRegistry);
7310
+ const adapter = selectAdapter(config.format, deps.adapterRegistry, deps.fs);
6885
7311
  const resolver = createLocalePathResolver(cwd, config);
6886
7312
  const source = await readSourceResource(config, resolver, fs, adapter);
6887
7313
  const format = input.format ?? DEFAULT_EXCHANGE_FORMAT;
@@ -6894,6 +7320,7 @@ async function importWorkbook(input, deps = {}) {
6894
7320
  const lock = await readLockFile(lockFilePath(cwd), fs);
6895
7321
  const ctx = {
6896
7322
  config,
7323
+ cwd,
6897
7324
  resolver,
6898
7325
  adapter,
6899
7326
  fs,
@@ -6924,6 +7351,9 @@ async function importWorkbook(input, deps = {}) {
6924
7351
  }
6925
7352
  summaries.push(summary);
6926
7353
  } catch (error) {
7354
+ if (error instanceof SdkError && error.code === "LOCK_FILE_INVALID") {
7355
+ throw error;
7356
+ }
6927
7357
  summaries.push(failureSummary(sheet.locale, error));
6928
7358
  }
6929
7359
  }
@@ -6983,6 +7413,7 @@ async function watch(input, deps = {}) {
6983
7413
  const cwd = input.cwd ?? process.cwd();
6984
7414
  const debounceMs = input.debounceMs ?? DEFAULT_DEBOUNCE_MS;
6985
7415
  const fs = deps.fs ?? defaultFs;
7416
+ selectLocales(input.config, input.locales);
6986
7417
  resolveRunConcurrency(input.concurrency, false, input.config);
6987
7418
  const resolver = createLocalePathResolver(cwd, input.config);
6988
7419
  const sourcePath = resolver.pathFor(input.config.sourceLocale);
@@ -6996,6 +7427,7 @@ async function watch(input, deps = {}) {
6996
7427
  const runInput = {
6997
7428
  config: input.config,
6998
7429
  cwd,
7430
+ ...input.locales !== void 0 ? { locales: input.locales } : {},
6999
7431
  ...input.onLockWait !== void 0 ? { onLockWait: input.onLockWait } : {},
7000
7432
  ...input.onProgress !== void 0 ? { onProgress: input.onProgress } : {},
7001
7433
  ...input.lockAcquireTimeoutMs !== void 0 ? { lockAcquireTimeoutMs: input.lockAcquireTimeoutMs } : {},
@@ -7080,6 +7512,7 @@ exports.createLocalePathResolver = createLocalePathResolver;
7080
7512
  exports.defineConfig = defineConfig;
7081
7513
  exports.diff = diff;
7082
7514
  exports.diffLocaleSnapshots = diffLocaleSnapshots;
7515
+ exports.doctor = doctor;
7083
7516
  exports.editEntry = editEntry;
7084
7517
  exports.exportWorkbook = exportWorkbook;
7085
7518
  exports.importWorkbook = importWorkbook;
@@ -7089,11 +7522,13 @@ exports.loadConfig = loadConfig;
7089
7522
  exports.loadConfigWithMeta = loadConfigWithMeta;
7090
7523
  exports.loadLockFile = loadLockFile;
7091
7524
  exports.lockState = lockState;
7525
+ exports.readGlossaryFile = readGlossaryFile;
7092
7526
  exports.readLocaleFileSnapshot = readLocaleFileSnapshot;
7093
7527
  exports.retranslateEntry = retranslateEntry;
7094
7528
  exports.runStatus = runStatus;
7095
7529
  exports.scaffoldingMetadata = scaffoldingMetadata;
7096
7530
  exports.translate = translate2;
7531
+ exports.updateGlossaryTerm = updateGlossaryTerm;
7097
7532
  exports.verbatraConfigSchema = verbatraConfigSchema;
7098
7533
  exports.watch = watch;
7099
7534
  //# sourceMappingURL=index.cjs.map