@verbatra/sdk 0.1.0 → 0.2.1

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
@@ -12,6 +12,8 @@ var genai = require('@google/genai');
12
12
  var OpenAI = require('openai');
13
13
  var promises = require('fs/promises');
14
14
  var icuMessageformatParser = require('@formatjs/icu-messageformat-parser');
15
+ var ExcelJS = require('exceljs');
16
+ var JSZip = require('jszip');
15
17
  var chokidar = require('chokidar');
16
18
 
17
19
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -38,6 +40,8 @@ var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
38
40
  var deepl__namespace = /*#__PURE__*/_interopNamespace(deepl);
39
41
  var log__default = /*#__PURE__*/_interopDefault(log);
40
42
  var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
43
+ var ExcelJS__default = /*#__PURE__*/_interopDefault(ExcelJS);
44
+ var JSZip__default = /*#__PURE__*/_interopDefault(JSZip);
41
45
 
42
46
  // src/config/define-config.ts
43
47
  function defineConfig(config) {
@@ -907,9 +911,41 @@ async function readBounded(path, maxBytes) {
907
911
  await handle.close();
908
912
  }
909
913
  }
914
+ async function readBoundedBytesInto(handle, size) {
915
+ const buffer = Buffer.allocUnsafeSlow(size);
916
+ let offset = 0;
917
+ while (offset < size) {
918
+ const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
919
+ if (bytesRead === 0) {
920
+ break;
921
+ }
922
+ offset += bytesRead;
923
+ }
924
+ return new Uint8Array(buffer.buffer, buffer.byteOffset, offset);
925
+ }
926
+ async function readBoundedBytes(path, maxBytes) {
927
+ let handle;
928
+ try {
929
+ handle = await promises.open(path, "r");
930
+ } catch {
931
+ return { kind: "missing" };
932
+ }
933
+ try {
934
+ const info = await handle.stat();
935
+ if (!info.isFile()) {
936
+ return { kind: "missing" };
937
+ }
938
+ if (info.size > maxBytes) {
939
+ return { kind: "too-large" };
940
+ }
941
+ return { kind: "ok", bytes: await readBoundedBytesInto(handle, info.size) };
942
+ } finally {
943
+ await handle.close();
944
+ }
945
+ }
910
946
  async function atomicWrite(path$1, data) {
911
947
  const tmp = path.join(path.dirname(path$1), `.${path.basename(path$1)}.tmp-${process.pid}-${Date.now()}`);
912
- await promises.writeFile(tmp, data, "utf8");
948
+ await (typeof data === "string" ? promises.writeFile(tmp, data, "utf8") : promises.writeFile(tmp, data));
913
949
  try {
914
950
  await promises.rename(tmp, path$1);
915
951
  } catch (error) {
@@ -927,7 +963,9 @@ var defaultFs = {
927
963
  }
928
964
  },
929
965
  readFileBounded: (path, maxBytes) => readBounded(path, maxBytes),
930
- writeFile: (path, data) => atomicWrite(path, data)
966
+ readBytesBounded: (path, maxBytes) => readBoundedBytes(path, maxBytes),
967
+ writeFile: (path, data) => atomicWrite(path, data),
968
+ writeBytes: (path, data) => atomicWrite(path, data)
931
969
  };
932
970
  var LOCK_FILE_NAME = "verbatra.lock.json";
933
971
  var CURRENT_VERSION = 1;
@@ -1174,6 +1212,7 @@ function createJsonFileAdapter(options) {
1174
1212
  deriveEntry,
1175
1213
  extractPlaceholders: extractPlaceholders2,
1176
1214
  computeInvalidIcuKeys: computeInvalidIcuKeys2,
1215
+ validateMessage: validateMessage2,
1177
1216
  validateTree,
1178
1217
  buildWriteTree
1179
1218
  } = options;
@@ -1181,6 +1220,8 @@ function createJsonFileAdapter(options) {
1181
1220
  format,
1182
1221
  canHandle,
1183
1222
  extractPlaceholders: extractPlaceholders2,
1223
+ // Non-ICU formats supply no validator: every value is valid for their syntax.
1224
+ validateMessage: validateMessage2 ?? (() => true),
1184
1225
  async read(filePath, locale) {
1185
1226
  const outcome = await readBounded2(filePath);
1186
1227
  if (outcome.kind === "not-a-file") {
@@ -1296,10 +1337,13 @@ function analyzeIcuValue(value) {
1296
1337
  function extractPlaceholders(value) {
1297
1338
  return analyzeIcuValue(value).placeholders;
1298
1339
  }
1340
+ function validateMessage(value) {
1341
+ return analyzeIcuValue(value).valid;
1342
+ }
1299
1343
  function computeInvalidIcuKeys(entries) {
1300
1344
  const invalid = [];
1301
1345
  for (const [key, entry] of entries) {
1302
- if (!analyzeIcuValue(entry.value).valid) {
1346
+ if (!validateMessage(entry.value)) {
1303
1347
  invalid.push(key);
1304
1348
  }
1305
1349
  }
@@ -1313,7 +1357,8 @@ function createNextIntlJsonAdapter() {
1313
1357
  const analysis = analyzeIcuValue(value);
1314
1358
  return { placeholders: analysis.placeholders, isPlural: analysis.isPlural };
1315
1359
  },
1316
- computeInvalidIcuKeys
1360
+ computeInvalidIcuKeys,
1361
+ validateMessage
1317
1362
  });
1318
1363
  }
1319
1364
  function assertNotMixed(tree) {
@@ -1480,6 +1525,33 @@ function selectProvider(config, createProvider = buildProvider) {
1480
1525
  }
1481
1526
  }
1482
1527
 
1528
+ // src/flow/locale-failure.ts
1529
+ function describeError(error) {
1530
+ if (error instanceof Error) {
1531
+ const code = error.code;
1532
+ return { code: typeof code === "string" ? code : "LOCALE_FAILED", message: error.message };
1533
+ }
1534
+ return { code: "LOCALE_FAILED", message: String(error) };
1535
+ }
1536
+ function failureSummary(locale, error) {
1537
+ return {
1538
+ locale,
1539
+ status: "failed",
1540
+ translated: [],
1541
+ unchanged: [],
1542
+ orphaned: [],
1543
+ invalidIcuSource: [],
1544
+ integrityMismatches: [],
1545
+ notices: [],
1546
+ error: describeError(error)
1547
+ };
1548
+ }
1549
+ function partition(locales) {
1550
+ const succeeded = locales.filter((s) => s.status === "succeeded").map((s) => s.locale);
1551
+ const failed = locales.filter((s) => s.status === "failed").map((s) => s.locale);
1552
+ return { succeeded, failed };
1553
+ }
1554
+
1483
1555
  // src/flow/notices.ts
1484
1556
  function isNotice(value) {
1485
1557
  return typeof value === "object" && value !== null && typeof value.code === "string" && typeof value.message === "string";
@@ -1605,27 +1677,7 @@ function computeLockEntries(params, merged, withheld) {
1605
1677
  return lockEntries;
1606
1678
  }
1607
1679
 
1608
- // src/flow/translate-project.ts
1609
- function describeError(error) {
1610
- if (error instanceof Error) {
1611
- const code = error.code;
1612
- return { code: typeof code === "string" ? code : "LOCALE_FAILED", message: error.message };
1613
- }
1614
- return { code: "LOCALE_FAILED", message: String(error) };
1615
- }
1616
- function failureSummary(locale, error) {
1617
- return {
1618
- locale,
1619
- status: "failed",
1620
- translated: [],
1621
- unchanged: [],
1622
- orphaned: [],
1623
- invalidIcuSource: [],
1624
- integrityMismatches: [],
1625
- notices: [],
1626
- error: describeError(error)
1627
- };
1628
- }
1680
+ // src/flow/source.ts
1629
1681
  async function readSource(config, cwd, fs, adapter) {
1630
1682
  const sourcePath = localeFilePath(cwd, config.files.pattern, config.sourceLocale);
1631
1683
  if (!await fs.fileExists(sourcePath)) {
@@ -1644,6 +1696,8 @@ async function readSource(config, cwd, fs, adapter) {
1644
1696
  );
1645
1697
  }
1646
1698
  }
1699
+
1700
+ // src/flow/translate-project.ts
1647
1701
  async function translate2(input, deps = {}) {
1648
1702
  const config = input.config;
1649
1703
  const cwd = input.cwd ?? process.cwd();
@@ -1682,12 +1736,594 @@ async function translate2(input, deps = {}) {
1682
1736
  summaries.push(failureSummary(targetLocale, error));
1683
1737
  }
1684
1738
  }
1685
- return aggregate(dryRun, summaries);
1739
+ const { succeeded, failed } = partition(summaries);
1740
+ return { dryRun, locales: summaries, succeeded, failed };
1686
1741
  }
1687
- function aggregate(dryRun, locales) {
1688
- const succeeded = locales.filter((s) => s.status === "succeeded").map((s) => s.locale);
1689
- const failed = locales.filter((s) => s.status === "failed").map((s) => s.locale);
1690
- return { dryRun, locales, succeeded, failed };
1742
+ var ExchangeError = class extends Error {
1743
+ code;
1744
+ constructor(code, message) {
1745
+ super(message);
1746
+ this.name = "ExchangeError";
1747
+ this.code = code;
1748
+ }
1749
+ };
1750
+ var INSTRUCTIONS_LINES = [
1751
+ "How to use this workbook",
1752
+ "",
1753
+ "1. There is one sheet per language. Open the sheet named for the language you are translating.",
1754
+ "2. Fill ONLY the 'Translation' column. Leave every other column unchanged.",
1755
+ "3. An empty 'Translation' cell means 'not translated yet'. It is skipped, never written as an empty string.",
1756
+ "4. Keep placeholders exactly as they appear. A token such as {name} or {count} must stay verbatim,",
1757
+ " in your translation, with the same spelling. Do not translate or remove it.",
1758
+ "5. For ICU messages (for example {count, plural, one {# item} other {# items}}), keep the ICU",
1759
+ " structure and the argument names exactly. Translate only the human-readable text.",
1760
+ "6. Do not edit the 'Key' column or the hidden 'Source hash' column. They map your translation",
1761
+ " back to the right string. You may sort or filter rows freely; mapping does not depend on row order.",
1762
+ "",
1763
+ "Status values:",
1764
+ " new - this string has no translation yet.",
1765
+ " changed - the source string changed and the translation needs updating.",
1766
+ "",
1767
+ "When you are done, save the file and send it back. verbatra checks every value on import:",
1768
+ "a value with a broken placeholder, invalid ICU, or a source that changed since export is",
1769
+ "withheld (not written) and reported so you can fix and resubmit it."
1770
+ ];
1771
+ var COLUMN = {
1772
+ key: 1,
1773
+ source: 2,
1774
+ current: 3,
1775
+ status: 4,
1776
+ translation: 5,
1777
+ sourceHash: 6
1778
+ };
1779
+ var HEADERS = [
1780
+ "Key",
1781
+ "Source",
1782
+ "Current translation",
1783
+ "Status",
1784
+ "Translation",
1785
+ "Source hash"
1786
+ ];
1787
+ var HEADER_ROW = 1;
1788
+ var INSTRUCTIONS_SHEET_NAME = "Instructions";
1789
+ var READ_ONLY_FILL = {
1790
+ type: "pattern",
1791
+ pattern: "solid",
1792
+ fgColor: { argb: "FFF1F3F5" }
1793
+ };
1794
+ var HEADER_FILL = {
1795
+ type: "pattern",
1796
+ pattern: "solid",
1797
+ fgColor: { argb: "FFDDE3EA" }
1798
+ };
1799
+ var COLUMN_WIDTHS = {
1800
+ [COLUMN.key]: 36,
1801
+ [COLUMN.source]: 50,
1802
+ [COLUMN.current]: 50,
1803
+ [COLUMN.status]: 12,
1804
+ [COLUMN.translation]: 50
1805
+ };
1806
+ function styleHeader(sheet) {
1807
+ const header = sheet.getRow(HEADER_ROW);
1808
+ HEADERS.forEach((label, index) => {
1809
+ const cell = header.getCell(index + 1);
1810
+ cell.value = label;
1811
+ cell.font = { bold: true };
1812
+ cell.fill = HEADER_FILL;
1813
+ });
1814
+ header.commit();
1815
+ }
1816
+ function applyColumnGeometry(sheet) {
1817
+ for (const [column, width] of Object.entries(COLUMN_WIDTHS)) {
1818
+ sheet.getColumn(Number(column)).width = width;
1819
+ }
1820
+ sheet.getColumn(COLUMN.sourceHash).hidden = true;
1821
+ sheet.views = [{ state: "frozen", ySplit: HEADER_ROW }];
1822
+ }
1823
+ function writeRow(sheet, sheetRow) {
1824
+ const row = sheet.addRow([]);
1825
+ row.getCell(COLUMN.key).value = sheetRow.key;
1826
+ row.getCell(COLUMN.source).value = sheetRow.source;
1827
+ row.getCell(COLUMN.current).value = sheetRow.currentTarget;
1828
+ row.getCell(COLUMN.status).value = sheetRow.status;
1829
+ row.getCell(COLUMN.translation).value = sheetRow.translation === "" ? null : sheetRow.translation;
1830
+ row.getCell(COLUMN.sourceHash).value = sheetRow.sourceHash;
1831
+ for (let column = COLUMN.key; column <= COLUMN.sourceHash; column += 1) {
1832
+ const cell = row.getCell(column);
1833
+ cell.protection = { locked: column !== COLUMN.translation };
1834
+ if (column !== COLUMN.translation) {
1835
+ cell.fill = READ_ONLY_FILL;
1836
+ }
1837
+ }
1838
+ row.commit();
1839
+ }
1840
+ var MAX_WORKSHEET_NAME_LENGTH = 31;
1841
+ var FORBIDDEN_WORKSHEET_NAME_CHARS = /[:\\/?*[\]]/;
1842
+ function assertValidWorksheetName(locale) {
1843
+ if (locale.length === 0 || locale.length > MAX_WORKSHEET_NAME_LENGTH) {
1844
+ throw new ExchangeError(
1845
+ "WORKBOOK_INVALID",
1846
+ `The locale "${locale}" cannot be an Excel worksheet name: it must be 1 to ${MAX_WORKSHEET_NAME_LENGTH} characters.`
1847
+ );
1848
+ }
1849
+ if (FORBIDDEN_WORKSHEET_NAME_CHARS.test(locale)) {
1850
+ throw new ExchangeError(
1851
+ "WORKBOOK_INVALID",
1852
+ `The locale "${locale}" cannot be an Excel worksheet name: it must not contain any of : \\ / ? * [ ].`
1853
+ );
1854
+ }
1855
+ }
1856
+ async function buildDataSheet(workbook, sheet) {
1857
+ assertValidWorksheetName(sheet.locale);
1858
+ const worksheet = workbook.addWorksheet(sheet.locale);
1859
+ styleHeader(worksheet);
1860
+ for (const row of sheet.rows) {
1861
+ writeRow(worksheet, row);
1862
+ }
1863
+ applyColumnGeometry(worksheet);
1864
+ await worksheet.protect("", {
1865
+ spinCount: 0,
1866
+ selectLockedCells: true,
1867
+ selectUnlockedCells: true,
1868
+ formatColumns: true,
1869
+ sort: true,
1870
+ autoFilter: true
1871
+ });
1872
+ }
1873
+ function buildInstructionsSheet(workbook) {
1874
+ const sheet = workbook.addWorksheet(INSTRUCTIONS_SHEET_NAME);
1875
+ sheet.getColumn(1).width = 110;
1876
+ for (const line of INSTRUCTIONS_LINES) {
1877
+ sheet.addRow([line]);
1878
+ }
1879
+ sheet.getRow(1).font = { bold: true };
1880
+ }
1881
+ async function buildWorkbook(model) {
1882
+ const workbook = new ExcelJS__default.default.Workbook();
1883
+ buildInstructionsSheet(workbook);
1884
+ for (const sheet of model.sheets) {
1885
+ await buildDataSheet(workbook, sheet);
1886
+ }
1887
+ try {
1888
+ const buffer = await workbook.xlsx.writeBuffer();
1889
+ const view = buffer;
1890
+ return Uint8Array.prototype.slice.call(view);
1891
+ } catch {
1892
+ throw new ExchangeError("WORKBOOK_INVALID", "The workbook could not be serialized.");
1893
+ }
1894
+ }
1895
+ var DEFAULT_WORKBOOK_LIMITS = {
1896
+ maxDecompressedBytes: 64 * 1024 * 1024,
1897
+ maxEntryCount: 1024,
1898
+ maxSheetCount: 256,
1899
+ maxRowsPerSheet: 1e5,
1900
+ maxCellsPerRow: 64
1901
+ };
1902
+ function assertNoDoctype(name, xml) {
1903
+ if (/<!DOCTYPE/i.test(xml) || /<!ENTITY/i.test(xml)) {
1904
+ throw new ExchangeError(
1905
+ "WORKBOOK_INVALID",
1906
+ `A workbook XML part (${name}) declares a DTD or entity, which is not permitted.`
1907
+ );
1908
+ }
1909
+ }
1910
+ function declaredSize(file) {
1911
+ const data = file._data;
1912
+ const size = data?.uncompressedSize;
1913
+ return typeof size === "number" && Number.isFinite(size) ? size : void 0;
1914
+ }
1915
+ async function guardWorkbookBytes(bytes, limits) {
1916
+ let zip;
1917
+ try {
1918
+ zip = await JSZip__default.default.loadAsync(bytes);
1919
+ } catch {
1920
+ throw new ExchangeError("WORKBOOK_INVALID", "The workbook is not a readable xlsx container.");
1921
+ }
1922
+ const files = Object.values(zip.files).filter((file) => !file.dir);
1923
+ if (files.length > limits.maxEntryCount) {
1924
+ throw new ExchangeError(
1925
+ "WORKBOOK_INVALID",
1926
+ `The workbook has more than the maximum of ${limits.maxEntryCount} entries.`
1927
+ );
1928
+ }
1929
+ let declaredTotal = 0;
1930
+ for (const file of files) {
1931
+ const size = declaredSize(file);
1932
+ if (size !== void 0) {
1933
+ declaredTotal += size;
1934
+ if (declaredTotal > limits.maxDecompressedBytes) {
1935
+ throw new ExchangeError(
1936
+ "WORKBOOK_INVALID",
1937
+ `The workbook decompresses to more than the maximum of ${limits.maxDecompressedBytes} bytes.`
1938
+ );
1939
+ }
1940
+ }
1941
+ }
1942
+ let actualTotal = 0;
1943
+ for (const file of files) {
1944
+ let content;
1945
+ try {
1946
+ content = await file.async("string");
1947
+ } catch {
1948
+ throw new ExchangeError("WORKBOOK_INVALID", "A workbook entry could not be decompressed.");
1949
+ }
1950
+ actualTotal += Buffer.byteLength(content, "utf8");
1951
+ if (actualTotal > limits.maxDecompressedBytes) {
1952
+ throw new ExchangeError(
1953
+ "WORKBOOK_INVALID",
1954
+ `The workbook decompresses to more than the maximum of ${limits.maxDecompressedBytes} bytes.`
1955
+ );
1956
+ }
1957
+ assertNoDoctype(file.name, content);
1958
+ }
1959
+ }
1960
+ var rowSchema = zod.z.object({
1961
+ key: zod.z.string().min(1),
1962
+ source: zod.z.string(),
1963
+ currentTarget: zod.z.string(),
1964
+ status: zod.z.enum(["new", "changed"]),
1965
+ sourceHash: zod.z.string(),
1966
+ translation: zod.z.string()
1967
+ });
1968
+ function cellString(cell) {
1969
+ const value = cell.value;
1970
+ if (value === null || value === void 0) {
1971
+ return "";
1972
+ }
1973
+ if (typeof value === "string") {
1974
+ return value;
1975
+ }
1976
+ if (typeof value === "number" || typeof value === "boolean") {
1977
+ return String(value);
1978
+ }
1979
+ return typeof cell.text === "string" ? cell.text : "";
1980
+ }
1981
+ function assertHeader(sheet) {
1982
+ const header = sheet.getRow(HEADER_ROW);
1983
+ const key = cellString(header.getCell(COLUMN.key));
1984
+ const sourceHash = cellString(header.getCell(COLUMN.sourceHash));
1985
+ if (key !== HEADERS[COLUMN.key - 1] || sourceHash !== HEADERS[COLUMN.sourceHash - 1]) {
1986
+ throw new ExchangeError(
1987
+ "WORKBOOK_INVALID",
1988
+ `The sheet "${sheet.name}" is missing the expected Key and Source hash columns.`
1989
+ );
1990
+ }
1991
+ }
1992
+ function parseRow(sheet, row) {
1993
+ const candidate = {
1994
+ key: cellString(row.getCell(COLUMN.key)),
1995
+ source: cellString(row.getCell(COLUMN.source)),
1996
+ currentTarget: cellString(row.getCell(COLUMN.current)),
1997
+ status: cellString(row.getCell(COLUMN.status)),
1998
+ sourceHash: cellString(row.getCell(COLUMN.sourceHash)),
1999
+ translation: cellString(row.getCell(COLUMN.translation))
2000
+ };
2001
+ const result = rowSchema.safeParse(candidate);
2002
+ if (!result.success) {
2003
+ throw new ExchangeError(
2004
+ "WORKBOOK_INVALID",
2005
+ `The sheet "${sheet.name}" has a row with a missing key or an unrecognized status.`
2006
+ );
2007
+ }
2008
+ return result.data;
2009
+ }
2010
+ function readDataSheet(sheet, limits) {
2011
+ assertHeader(sheet);
2012
+ if (sheet.rowCount - HEADER_ROW > limits.maxRowsPerSheet) {
2013
+ throw new ExchangeError(
2014
+ "WORKBOOK_INVALID",
2015
+ `The sheet "${sheet.name}" has more than the maximum of ${limits.maxRowsPerSheet} rows.`
2016
+ );
2017
+ }
2018
+ const rows = [];
2019
+ for (let rowNumber = HEADER_ROW + 1; rowNumber <= sheet.rowCount; rowNumber += 1) {
2020
+ const row = sheet.getRow(rowNumber);
2021
+ if (row.cellCount > limits.maxCellsPerRow) {
2022
+ throw new ExchangeError(
2023
+ "WORKBOOK_INVALID",
2024
+ `The sheet "${sheet.name}" has a row with more than the maximum of ${limits.maxCellsPerRow} cells.`
2025
+ );
2026
+ }
2027
+ if (cellString(row.getCell(COLUMN.key)) === "") {
2028
+ continue;
2029
+ }
2030
+ rows.push(parseRow(sheet, row));
2031
+ }
2032
+ return { locale: sheet.name, rows };
2033
+ }
2034
+ async function loadWorkbook(bytes) {
2035
+ const workbook = new ExcelJS__default.default.Workbook();
2036
+ try {
2037
+ const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2038
+ await workbook.xlsx.load(buffer);
2039
+ } catch {
2040
+ throw new ExchangeError("WORKBOOK_INVALID", "The workbook could not be parsed as xlsx.");
2041
+ }
2042
+ return workbook;
2043
+ }
2044
+ async function readWorkbook(bytes, options = {}) {
2045
+ const limits = options.limits ?? DEFAULT_WORKBOOK_LIMITS;
2046
+ await guardWorkbookBytes(bytes, limits);
2047
+ const workbook = await loadWorkbook(bytes);
2048
+ if (workbook.worksheets.length > limits.maxSheetCount) {
2049
+ throw new ExchangeError(
2050
+ "WORKBOOK_INVALID",
2051
+ `The workbook has more than the maximum of ${limits.maxSheetCount} sheets.`
2052
+ );
2053
+ }
2054
+ const sheets = [];
2055
+ for (const sheet of workbook.worksheets) {
2056
+ if (sheet.name === INSTRUCTIONS_SHEET_NAME) {
2057
+ continue;
2058
+ }
2059
+ sheets.push(readDataSheet(sheet, limits));
2060
+ }
2061
+ return { sheets };
2062
+ }
2063
+
2064
+ // src/flow/workbook/export-workbook.ts
2065
+ var DEFAULT_WORKBOOK_PATH = "verbatra-translations.xlsx";
2066
+ async function readTarget2(cwd, config, adapter, fs, locale) {
2067
+ const path = localeFilePath(cwd, config.files.pattern, locale);
2068
+ if (!await fs.fileExists(path)) {
2069
+ return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
2070
+ }
2071
+ return (await adapter.read(path, locale)).resource;
2072
+ }
2073
+ function buildRows(source, target, baseline, includeUnchanged) {
2074
+ const diff = diffResources(source, target, { baseline });
2075
+ const rows = [];
2076
+ const add = (keys, status) => {
2077
+ for (const key of keys) {
2078
+ const sourceEntry = source.entries.get(key);
2079
+ if (sourceEntry === void 0) {
2080
+ continue;
2081
+ }
2082
+ rows.push({
2083
+ key,
2084
+ source: sourceEntry.value,
2085
+ currentTarget: target.entries.get(key)?.value ?? "",
2086
+ status,
2087
+ sourceHash: contentHash(sourceEntry),
2088
+ translation: ""
2089
+ });
2090
+ }
2091
+ };
2092
+ add(diff.missing, "new");
2093
+ add(diff.changed, "changed");
2094
+ if (includeUnchanged) {
2095
+ add(diff.unchanged, "changed");
2096
+ }
2097
+ return [...rows].sort((a, b) => a.key < b.key ? -1 : 1);
2098
+ }
2099
+ function selectedLocales(config, requested) {
2100
+ if (requested === void 0) {
2101
+ return config.targetLocales;
2102
+ }
2103
+ const wanted = new Set(requested);
2104
+ return config.targetLocales.filter((locale) => wanted.has(locale));
2105
+ }
2106
+ async function exportWorkbook(input, deps = {}) {
2107
+ const config = input.config;
2108
+ const cwd = input.cwd ?? process.cwd();
2109
+ const fs = deps.fs ?? defaultFs;
2110
+ const adapter = selectAdapter(config.format, deps.adapterRegistry);
2111
+ const source = await readSource(config, cwd, fs, adapter);
2112
+ const lock = await readLockFile(lockFilePath(cwd), fs);
2113
+ const locales = selectedLocales(config, input.locales);
2114
+ const sheets = await Promise.all(
2115
+ locales.map(async (locale) => {
2116
+ const target = await readTarget2(cwd, config, adapter, fs, locale);
2117
+ const rows = buildRows(
2118
+ source.resource,
2119
+ target,
2120
+ baselineFor(lock, locale),
2121
+ input.includeUnchanged ?? false
2122
+ );
2123
+ return { locale, rows };
2124
+ })
2125
+ );
2126
+ const model = { sheets };
2127
+ const bytes = await buildWorkbook(model);
2128
+ const path$1 = path.resolve(cwd, input.out ?? DEFAULT_WORKBOOK_PATH);
2129
+ await fs.writeBytes(path$1, bytes);
2130
+ return {
2131
+ path: path$1,
2132
+ locales: sheets.map((sheet) => ({ locale: sheet.locale, rows: sheet.rows.length }))
2133
+ };
2134
+ }
2135
+
2136
+ // src/flow/workbook/import-locale.ts
2137
+ var UnknownKeyError = class extends Error {
2138
+ key;
2139
+ constructor(key) {
2140
+ super(`The workbook has a row with key "${key}" that maps to no known source or target key.`);
2141
+ this.name = "UnknownKeyError";
2142
+ this.key = key;
2143
+ }
2144
+ };
2145
+ function isUnknownKey(row, source, target) {
2146
+ return !source.entries.has(row.key) && !target.entries.has(row.key);
2147
+ }
2148
+ function judge(row, sourceEntry, adapter) {
2149
+ if (contentHash(sourceEntry) !== row.sourceHash) {
2150
+ return "drift";
2151
+ }
2152
+ const integrity = checkPlaceholders(
2153
+ sourceEntry.placeholders,
2154
+ adapter.extractPlaceholders(row.translation)
2155
+ );
2156
+ if (!integrity.matches) {
2157
+ return "placeholder";
2158
+ }
2159
+ if (!adapter.validateMessage(row.translation)) {
2160
+ return "icu";
2161
+ }
2162
+ return void 0;
2163
+ }
2164
+ function classifyRows(params, buckets) {
2165
+ for (const row of params.sheet.rows) {
2166
+ if (row.translation === "") {
2167
+ continue;
2168
+ }
2169
+ if (isUnknownKey(row, params.source, params.target)) {
2170
+ throw new UnknownKeyError(row.key);
2171
+ }
2172
+ const sourceEntry = params.source.entries.get(row.key);
2173
+ if (sourceEntry === void 0) {
2174
+ continue;
2175
+ }
2176
+ const reason = judge(row, sourceEntry, params.adapter);
2177
+ if (reason === void 0) {
2178
+ buckets.accepted.set(row.key, { value: row.translation, source: sourceEntry });
2179
+ } else {
2180
+ buckets.mismatches.push(row.key);
2181
+ buckets.withheld.add(row.key);
2182
+ }
2183
+ }
2184
+ }
2185
+ function importLocale(params) {
2186
+ const diff = diffResources(params.source, params.target, { baseline: params.baseline });
2187
+ const buckets = { accepted: /* @__PURE__ */ new Map(), mismatches: [], withheld: /* @__PURE__ */ new Set() };
2188
+ classifyRows(params, buckets);
2189
+ const rowKeys = new Set(params.sheet.rows.map((row) => row.key));
2190
+ const invalidIcuSource = [...new Set(params.sourceInvalidIcuKeys)].filter((key) => rowKeys.has(key)).sort();
2191
+ const summary = {
2192
+ locale: params.sheet.locale,
2193
+ status: "succeeded",
2194
+ translated: [...buckets.accepted.keys()].sort(),
2195
+ unchanged: diff.unchanged,
2196
+ orphaned: diff.orphaned,
2197
+ invalidIcuSource,
2198
+ integrityMismatches: [...buckets.mismatches].sort(),
2199
+ notices: []
2200
+ };
2201
+ return { summary, accepted: buckets.accepted, withheld: buckets.withheld };
2202
+ }
2203
+
2204
+ // src/flow/workbook/import-workbook.ts
2205
+ var MAX_WORKBOOK_FILE_BYTES = 64 * 1024 * 1024;
2206
+ async function readWorkbookBytes(path, fs) {
2207
+ const read = await fs.readBytesBounded(path, MAX_WORKBOOK_FILE_BYTES);
2208
+ if (read.kind === "missing") {
2209
+ throw new SdkError("SOURCE_UNREADABLE", `The workbook was not found at ${path}.`);
2210
+ }
2211
+ if (read.kind === "too-large") {
2212
+ throw new SdkError(
2213
+ "SOURCE_INVALID",
2214
+ `The workbook at ${path} exceeds the maximum allowed size of ${MAX_WORKBOOK_FILE_BYTES} bytes.`
2215
+ );
2216
+ }
2217
+ return read.bytes;
2218
+ }
2219
+ async function readTarget3(cwd, config, adapter, fs, locale) {
2220
+ const path = localeFilePath(cwd, config.files.pattern, locale);
2221
+ if (!await fs.fileExists(path)) {
2222
+ return { locale, namespace: "", format: config.format, entries: /* @__PURE__ */ new Map() };
2223
+ }
2224
+ return (await adapter.read(path, locale)).resource;
2225
+ }
2226
+ function mergeAccepted(target, accepted) {
2227
+ const merged = new Map(target.entries);
2228
+ for (const [key, { value, source }] of accepted) {
2229
+ merged.set(key, { ...source, value, namespace: target.namespace });
2230
+ }
2231
+ return merged;
2232
+ }
2233
+ function computeLockEntries2(source, merged, baseline, withheld) {
2234
+ const entries = {};
2235
+ for (const key of merged.keys()) {
2236
+ const sourceEntry = source.entries.get(key);
2237
+ if (sourceEntry === void 0) {
2238
+ continue;
2239
+ }
2240
+ if (withheld.has(key)) {
2241
+ const prior = baseline.get(key);
2242
+ if (prior !== void 0) {
2243
+ entries[key] = prior;
2244
+ }
2245
+ continue;
2246
+ }
2247
+ entries[key] = contentHash(sourceEntry);
2248
+ }
2249
+ return entries;
2250
+ }
2251
+ async function runSheet(ctx, sheet, lock) {
2252
+ if (!ctx.config.targetLocales.includes(sheet.locale)) {
2253
+ throw new SdkError(
2254
+ "CONFIG_INVALID",
2255
+ `The workbook has a sheet for locale "${sheet.locale}", which is not a configured target locale.`
2256
+ );
2257
+ }
2258
+ const target = await readTarget3(ctx.cwd, ctx.config, ctx.adapter, ctx.fs, sheet.locale);
2259
+ const baseline = baselineFor(lock, sheet.locale);
2260
+ const { summary, accepted, withheld } = importLocale({
2261
+ sheet,
2262
+ source: ctx.source,
2263
+ target,
2264
+ baseline,
2265
+ adapter: ctx.adapter,
2266
+ sourceInvalidIcuKeys: ctx.sourceInvalidIcuKeys
2267
+ });
2268
+ if (ctx.dryRun) {
2269
+ return { summary, lockEntries: {} };
2270
+ }
2271
+ const merged = mergeAccepted(target, accepted);
2272
+ if (accepted.size > 0) {
2273
+ const path = localeFilePath(ctx.cwd, ctx.config.files.pattern, sheet.locale);
2274
+ await ctx.adapter.write(
2275
+ {
2276
+ locale: sheet.locale,
2277
+ namespace: target.namespace,
2278
+ format: ctx.config.format,
2279
+ entries: merged
2280
+ },
2281
+ path
2282
+ );
2283
+ }
2284
+ return { summary, lockEntries: computeLockEntries2(ctx.source, merged, baseline, withheld) };
2285
+ }
2286
+ async function importWorkbook(input, deps = {}) {
2287
+ const config = input.config;
2288
+ const cwd = input.cwd ?? process.cwd();
2289
+ const dryRun = input.dryRun ?? false;
2290
+ const fs = deps.fs ?? defaultFs;
2291
+ const adapter = selectAdapter(config.format, deps.adapterRegistry);
2292
+ const source = await readSource(config, cwd, fs, adapter);
2293
+ const workbookPath = path.resolve(cwd, input.workbook);
2294
+ const bytes = await readWorkbookBytes(workbookPath, fs);
2295
+ let data;
2296
+ try {
2297
+ data = await readWorkbook(bytes);
2298
+ } catch (error) {
2299
+ throw new SdkError("SOURCE_INVALID", error.message);
2300
+ }
2301
+ const lockPath = lockFilePath(cwd);
2302
+ let lock = await readLockFile(lockPath, fs);
2303
+ const ctx = {
2304
+ config,
2305
+ cwd,
2306
+ adapter,
2307
+ fs,
2308
+ source: source.resource,
2309
+ sourceInvalidIcuKeys: source.invalidIcuKeys,
2310
+ dryRun
2311
+ };
2312
+ const summaries = [];
2313
+ for (const sheet of data.sheets) {
2314
+ try {
2315
+ const { summary, lockEntries } = await runSheet(ctx, sheet, lock);
2316
+ if (!dryRun) {
2317
+ lock = updateLockLocale(lock, sheet.locale, lockEntries);
2318
+ await writeLockFile(lockPath, lock, fs);
2319
+ }
2320
+ summaries.push(summary);
2321
+ } catch (error) {
2322
+ summaries.push(failureSummary(sheet.locale, error));
2323
+ }
2324
+ }
2325
+ const { succeeded, failed } = partition(summaries);
2326
+ return { dryRun, locales: summaries, succeeded, failed };
1691
2327
  }
1692
2328
  var defaultCreateWatcher = (paths) => {
1693
2329
  const fsWatcher = chokidar.watch([...paths], { persistent: true, ignoreInitial: true });
@@ -1795,8 +2431,11 @@ async function watch(input, deps = {}) {
1795
2431
  return { stop };
1796
2432
  }
1797
2433
 
2434
+ exports.DEFAULT_WORKBOOK_PATH = DEFAULT_WORKBOOK_PATH;
1798
2435
  exports.SdkError = SdkError;
1799
2436
  exports.defineConfig = defineConfig;
2437
+ exports.exportWorkbook = exportWorkbook;
2438
+ exports.importWorkbook = importWorkbook;
1800
2439
  exports.loadConfig = loadConfig;
1801
2440
  exports.translate = translate2;
1802
2441
  exports.verbatraConfigSchema = verbatraConfigSchema;