@el-j/google-sheet-translations 2.1.4 → 2.2.0-beta.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.js CHANGED
@@ -39,19 +39,25 @@ __export(index_exports, {
39
39
  default: () => index_default,
40
40
  filterValidLocales: () => filterValidLocales,
41
41
  findLocalChanges: () => findLocalChanges,
42
+ getGoogleTranslateCode: () => getGoogleTranslateCode,
42
43
  getLanguagePrefix: () => getLanguagePrefix,
43
44
  getLocaleDisplayName: () => getLocaleDisplayName,
45
+ getMultipleSpreadSheetsData: () => getMultipleSpreadSheetsData,
44
46
  getNormalizedLocaleForHeader: () => getNormalizedLocaleForHeader,
45
47
  getOriginalHeaderForLocale: () => getOriginalHeaderForLocale,
46
48
  getSpreadSheetData: () => getSpreadSheetData,
47
49
  getTranslationSummary: () => getTranslationSummary,
48
50
  handleBidirectionalSync: () => handleBidirectionalSync,
49
51
  isValidLocale: () => isValidLocale,
52
+ manageDriveTranslations: () => manageDriveTranslations,
53
+ mergeMultipleTranslationData: () => mergeMultipleTranslationData,
50
54
  mergeSheets: () => mergeSheets,
51
55
  normalizeLocaleCode: () => normalizeLocaleCode,
52
56
  processRawRows: () => processRawRows,
53
57
  readPublicSheet: () => readPublicSheet,
54
58
  resolveLocaleWithFallback: () => resolveLocaleWithFallback,
59
+ scanDriveFolderForSpreadsheets: () => scanDriveFolderForSpreadsheets,
60
+ syncDriveImages: () => syncDriveImages,
55
61
  updateSpreadsheetWithLocalChanges: () => updateSpreadsheetWithLocalChanges,
56
62
  validateCredentials: () => validateCredentials,
57
63
  validateEnv: () => validateEnv,
@@ -234,6 +240,14 @@ function filterValidLocales(headerRow, keyColumn) {
234
240
  function getLanguagePrefix(locale) {
235
241
  return locale.toLowerCase().split(/[-_]/)[0];
236
242
  }
243
+ var GOOGLE_TRANSLATE_CODES_REQUIRING_REGION = /* @__PURE__ */ new Set(["zh-tw", "zh-cn"]);
244
+ function getGoogleTranslateCode(locale) {
245
+ const normalized = locale.toLowerCase().trim().replace("_", "-");
246
+ if (GOOGLE_TRANSLATE_CODES_REQUIRING_REGION.has(normalized)) {
247
+ return normalized;
248
+ }
249
+ return normalized.split(/[-_]/)[0];
250
+ }
237
251
  var LANGUAGE_TO_COUNTRY_MAP = {
238
252
  "en": "en-GB",
239
253
  "de": "de-DE",
@@ -596,12 +610,23 @@ function columnIndexToLetter(index) {
596
610
  } while (i >= 0);
597
611
  return result;
598
612
  }
599
- function langCodeFormula(cellRef) {
600
- return `LOWER(IFERROR(LEFT(${cellRef},FIND("-",${cellRef})-1),${cellRef}))`;
613
+ function getFormulaSeparator(doc) {
614
+ try {
615
+ const locale = doc._rawProperties?.locale || "";
616
+ if (/^(en|ja|ko|zh|th|id|ms)/i.test(locale)) return ",";
617
+ } catch {
618
+ }
619
+ return ";";
620
+ }
621
+ function langCodeFormula(cellRef, sep) {
622
+ const prefix = `LOWER(IFERROR(LEFT(${cellRef}${sep}FIND("-"${sep}${cellRef})-1)${sep}${cellRef}))`;
623
+ const full = `LOWER(${cellRef})`;
624
+ return `IF(LOWER(LEFT(${cellRef}${sep}3))="zh-"${sep}${full}${sep}${prefix})`;
601
625
  }
602
626
  async function updateSpreadsheetWithLocalChanges(doc, changes, waitSeconds, autoTranslate = false, localeMapping = {}, override = false) {
603
627
  console.log("Updating spreadsheet with local changes...");
604
628
  const baseDelayMs = waitSeconds * 1e3;
629
+ const sep = getFormulaSeparator(doc);
605
630
  for (const sheetTitle of new Set(
606
631
  Object.values(changes).flatMap((locale) => Object.keys(locale))
607
632
  )) {
@@ -745,7 +770,7 @@ async function updateSpreadsheetWithLocalChanges(doc, changes, waitSeconds, auto
745
770
  const targetColumnLetter = columnIndexToLetter(targetHeaderIndex);
746
771
  row.set(
747
772
  exactTargetHeader,
748
- `=GOOGLETRANSLATE(INDIRECT("${sourceColumnLetter}"&ROW());${langCodeFormula(`$${sourceColumnLetter}$1`)};${langCodeFormula(`${targetColumnLetter}$1`)})`
773
+ `=GOOGLETRANSLATE(INDIRECT("${sourceColumnLetter}"&ROW())${sep}${langCodeFormula(`$${sourceColumnLetter}$1`, sep)}${sep}${langCodeFormula(`${targetColumnLetter}$1`, sep)})`
749
774
  );
750
775
  }
751
776
  }
@@ -794,7 +819,7 @@ async function updateSpreadsheetWithLocalChanges(doc, changes, waitSeconds, auto
794
819
  }
795
820
  const sourceColumnLetter = columnIndexToLetter(sourceHeaderIndex);
796
821
  const targetColumnLetter = columnIndexToLetter(targetHeaderIndex);
797
- rowData[exactHeaderName] = `=GOOGLETRANSLATE(INDIRECT("${sourceColumnLetter}"&ROW());${langCodeFormula(`$${sourceColumnLetter}$1`)};${langCodeFormula(`${targetColumnLetter}$1`)})`;
822
+ rowData[exactHeaderName] = `=GOOGLETRANSLATE(INDIRECT("${sourceColumnLetter}"&ROW())${sep}${langCodeFormula(`$${sourceColumnLetter}$1`, sep)}${sep}${langCodeFormula(`${targetColumnLetter}$1`, sep)})`;
798
823
  }
799
824
  }
800
825
  }
@@ -1050,8 +1075,18 @@ function colLetter(index) {
1050
1075
  } while (i >= 0);
1051
1076
  return result;
1052
1077
  }
1053
- function langCodeFormula2(cellRef) {
1054
- return `LOWER(IFERROR(LEFT(${cellRef},FIND("-",${cellRef})-1),${cellRef}))`;
1078
+ function getFormulaSeparator2(doc) {
1079
+ try {
1080
+ const locale = doc._rawProperties?.locale || "";
1081
+ if (/^(en|ja|ko|zh|th|id|ms)/i.test(locale)) return ",";
1082
+ } catch {
1083
+ }
1084
+ return ";";
1085
+ }
1086
+ function langCodeFormula2(cellRef, sep) {
1087
+ const prefix = `LOWER(IFERROR(LEFT(${cellRef}${sep}FIND("-"${sep}${cellRef})-1)${sep}${cellRef}))`;
1088
+ const full = `LOWER(${cellRef})`;
1089
+ return `IF(LOWER(LEFT(${cellRef}${sep}3))="zh-"${sep}${full}${sep}${prefix})`;
1055
1090
  }
1056
1091
  var DEFAULT_TARGET_LOCALES = ["de", "fr", "es", "it", "pt", "ja", "zh"];
1057
1092
  var STARTER_KEYS = {
@@ -1133,11 +1168,12 @@ async function createSpreadsheet(authClient, options = {}) {
1133
1168
  "setHeaderRow"
1134
1169
  );
1135
1170
  const sourceColLetter = colLetter(1);
1171
+ const sep = getFormulaSeparator2(doc);
1136
1172
  const rows = Object.entries(seedKeys).map(([key, sourceValue]) => {
1137
1173
  const row = { key, [sourceLocale]: sourceValue };
1138
1174
  targetLocales.forEach((locale, idx) => {
1139
1175
  const targetColLetter = colLetter(2 + idx);
1140
- row[locale] = `=GOOGLETRANSLATE(INDIRECT("${sourceColLetter}"&ROW());${langCodeFormula2(`$${sourceColLetter}$1`)};${langCodeFormula2(`${targetColLetter}$1`)})`;
1176
+ row[locale] = `=GOOGLETRANSLATE(INDIRECT("${sourceColLetter}"&ROW())${sep}${langCodeFormula2(`$${sourceColLetter}$1`, sep)}${sep}${langCodeFormula2(`${targetColLetter}$1`, sep)})`;
1141
1177
  });
1142
1178
  return row;
1143
1179
  });
@@ -1352,6 +1388,367 @@ function mergeSheets(translations, locale, sheetNames) {
1352
1388
  return merged;
1353
1389
  }
1354
1390
 
1391
+ // src/utils/multiSpreadsheetMerger.ts
1392
+ function mergeMultipleTranslationData(results, mergeStrategy = "later-wins") {
1393
+ const merged = {};
1394
+ for (const result of results) {
1395
+ for (const [locale, sheets] of Object.entries(result)) {
1396
+ if (!merged[locale]) {
1397
+ merged[locale] = {};
1398
+ }
1399
+ for (const [sheet, keys] of Object.entries(sheets)) {
1400
+ if (!merged[locale][sheet]) {
1401
+ merged[locale][sheet] = {};
1402
+ }
1403
+ for (const [key, value] of Object.entries(keys)) {
1404
+ if (mergeStrategy === "first-wins" && key in merged[locale][sheet]) {
1405
+ continue;
1406
+ }
1407
+ merged[locale][sheet][key] = value;
1408
+ }
1409
+ }
1410
+ }
1411
+ }
1412
+ return merged;
1413
+ }
1414
+
1415
+ // src/getMultipleSpreadSheetsData.ts
1416
+ async function getMultipleSpreadSheetsData(docTitles, options = {}) {
1417
+ const { spreadsheetIds, mergeStrategy = "later-wins", ...baseOptions } = options;
1418
+ if (!spreadsheetIds || spreadsheetIds.length === 0) {
1419
+ return getSpreadSheetData(docTitles, baseOptions);
1420
+ }
1421
+ console.log(`[getMultipleSpreadSheetsData] Fetching ${spreadsheetIds.length} spreadsheets...`);
1422
+ const results = [];
1423
+ for (let i = 0; i < spreadsheetIds.length; i++) {
1424
+ const id = spreadsheetIds[i];
1425
+ console.log(`[getMultipleSpreadSheetsData] (${i + 1}/${spreadsheetIds.length}) "${id}"...`);
1426
+ const result = await getSpreadSheetData(docTitles, { ...baseOptions, spreadsheetId: id });
1427
+ results.push(result);
1428
+ }
1429
+ return mergeMultipleTranslationData(results, mergeStrategy);
1430
+ }
1431
+
1432
+ // src/utils/driveFolderScanner.ts
1433
+ var import_google_auth_library2 = require("google-auth-library");
1434
+ var SPREADSHEET_MIME = "application/vnd.google-apps.spreadsheet";
1435
+ var FOLDER_MIME = "application/vnd.google-apps.folder";
1436
+ var DRIVE_FILES_URL = "https://www.googleapis.com/drive/v3/files";
1437
+ async function getAccessToken(credentials) {
1438
+ const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1439
+ const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
1440
+ if (!clientEmail || !privateKey) {
1441
+ throw new Error(
1442
+ "Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
1443
+ );
1444
+ }
1445
+ const normalizedKey = privateKey.replace(/\\n/g, "\n");
1446
+ const auth = new import_google_auth_library2.GoogleAuth({
1447
+ credentials: { client_email: clientEmail, private_key: normalizedKey },
1448
+ scopes: ["https://www.googleapis.com/auth/drive.readonly"]
1449
+ });
1450
+ const client = await auth.getClient();
1451
+ const tokenResponse = await client.getAccessToken();
1452
+ return tokenResponse.token;
1453
+ }
1454
+ async function listFilesInFolder(folderId, mimeType, token) {
1455
+ const results = [];
1456
+ let pageToken;
1457
+ do {
1458
+ const query = `'${folderId}' in parents and mimeType = '${mimeType}' and trashed = false`;
1459
+ const params = new URLSearchParams({
1460
+ q: query,
1461
+ fields: "nextPageToken,files(id,name,mimeType,modifiedTime,parents)",
1462
+ pageSize: "1000"
1463
+ });
1464
+ if (pageToken) params.set("pageToken", pageToken);
1465
+ const response = await fetch(`${DRIVE_FILES_URL}?${params.toString()}`, {
1466
+ headers: { Authorization: `Bearer ${token}` }
1467
+ });
1468
+ if (!response.ok) {
1469
+ const text = await response.text();
1470
+ throw new Error(
1471
+ `Drive API error ${response.status}: ${text}`
1472
+ );
1473
+ }
1474
+ const data = await response.json();
1475
+ results.push(...data.files);
1476
+ pageToken = data.nextPageToken;
1477
+ } while (pageToken);
1478
+ return results;
1479
+ }
1480
+ async function scanFolder(folderId, folderPath, token, recursive, nameFilter, seen = /* @__PURE__ */ new Set()) {
1481
+ console.log(`[driveFolderScanner] Scanning folder: ${folderId} (path: "${folderPath}")`);
1482
+ const spreadsheets = await listFilesInFolder(folderId, SPREADSHEET_MIME, token);
1483
+ const results = [];
1484
+ for (const file of spreadsheets) {
1485
+ if (seen.has(file.id)) continue;
1486
+ seen.add(file.id);
1487
+ if (nameFilter && !nameFilter.test(file.name)) continue;
1488
+ results.push({
1489
+ id: file.id,
1490
+ name: file.name,
1491
+ folderPath,
1492
+ mimeType: file.mimeType,
1493
+ modifiedTime: file.modifiedTime
1494
+ });
1495
+ }
1496
+ if (recursive) {
1497
+ const subfolders = await listFilesInFolder(folderId, FOLDER_MIME, token);
1498
+ for (const folder of subfolders) {
1499
+ const subPath = folderPath ? `${folderPath}/${folder.name}` : folder.name;
1500
+ const subResults = await scanFolder(
1501
+ folder.id,
1502
+ subPath,
1503
+ token,
1504
+ recursive,
1505
+ nameFilter,
1506
+ seen
1507
+ );
1508
+ results.push(...subResults);
1509
+ }
1510
+ }
1511
+ return results;
1512
+ }
1513
+ async function scanDriveFolderForSpreadsheets(options) {
1514
+ const { folderId, recursive = true, nameFilter, credentials } = options;
1515
+ const token = await getAccessToken(credentials);
1516
+ return scanFolder(folderId, "", token, recursive, nameFilter);
1517
+ }
1518
+
1519
+ // src/utils/driveImageSync.ts
1520
+ var import_node_fs6 = require("node:fs");
1521
+ var import_node_path5 = require("node:path");
1522
+ var import_promises4 = require("node:stream/promises");
1523
+ var import_node_stream = require("node:stream");
1524
+ var import_google_auth_library3 = require("google-auth-library");
1525
+ var DEFAULT_IMAGE_MIME_TYPES = [
1526
+ "image/jpeg",
1527
+ "image/jpg",
1528
+ "image/png",
1529
+ "image/webp",
1530
+ "image/avif",
1531
+ "image/gif",
1532
+ "image/svg+xml",
1533
+ "image/tiff",
1534
+ "image/bmp",
1535
+ "image/ico",
1536
+ "image/x-icon"
1537
+ ];
1538
+ var FOLDER_MIME2 = "application/vnd.google-apps.folder";
1539
+ var DRIVE_FILES_URL2 = "https://www.googleapis.com/drive/v3/files";
1540
+ async function getAccessToken2(credentials) {
1541
+ const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1542
+ const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
1543
+ if (!clientEmail || !privateKey) {
1544
+ throw new Error(
1545
+ "Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
1546
+ );
1547
+ }
1548
+ const normalizedKey = privateKey.replace(/\\n/g, "\n");
1549
+ const auth = new import_google_auth_library3.GoogleAuth({
1550
+ credentials: { client_email: clientEmail, private_key: normalizedKey },
1551
+ scopes: ["https://www.googleapis.com/auth/drive.readonly"]
1552
+ });
1553
+ const client = await auth.getClient();
1554
+ const tokenResponse = await client.getAccessToken();
1555
+ return tokenResponse.token;
1556
+ }
1557
+ async function listFilesInFolder2(folderId, token, mimeTypeFilter) {
1558
+ const results = [];
1559
+ let pageToken;
1560
+ do {
1561
+ const mimeClause = mimeTypeFilter ? ` and mimeType = '${mimeTypeFilter}'` : "";
1562
+ const query = `'${folderId}' in parents${mimeClause} and trashed = false`;
1563
+ const params = new URLSearchParams({
1564
+ q: query,
1565
+ fields: "nextPageToken,files(id,name,mimeType,parents)",
1566
+ pageSize: "1000"
1567
+ });
1568
+ if (pageToken) params.set("pageToken", pageToken);
1569
+ const response = await fetch(`${DRIVE_FILES_URL2}?${params.toString()}`, {
1570
+ headers: { Authorization: `Bearer ${token}` }
1571
+ });
1572
+ if (!response.ok) {
1573
+ const text = await response.text();
1574
+ throw new Error(`Drive API error ${response.status}: ${text}`);
1575
+ }
1576
+ const data = await response.json();
1577
+ results.push(...data.files);
1578
+ pageToken = data.nextPageToken;
1579
+ } while (pageToken);
1580
+ return results;
1581
+ }
1582
+ async function collectFiles(folderId, folderRelPath, outputPath, token, allowedMimeTypes, recursive, folderPattern) {
1583
+ console.log(`[driveImageSync] Scanning folder: ${folderId} (path: "${folderRelPath}")`);
1584
+ const allItems = await listFilesInFolder2(folderId, token);
1585
+ const entries = [];
1586
+ for (const item of allItems) {
1587
+ if (item.mimeType === FOLDER_MIME2) {
1588
+ if (!recursive) continue;
1589
+ const subRelPath = folderRelPath ? `${folderRelPath}/${item.name}` : item.name;
1590
+ if (folderPattern && !folderPattern.test(subRelPath)) continue;
1591
+ const subEntries = await collectFiles(
1592
+ item.id,
1593
+ subRelPath,
1594
+ outputPath,
1595
+ token,
1596
+ allowedMimeTypes,
1597
+ recursive,
1598
+ folderPattern
1599
+ );
1600
+ entries.push(...subEntries);
1601
+ } else if (allowedMimeTypes.includes(item.mimeType)) {
1602
+ const localPath = folderRelPath ? (0, import_node_path5.join)(outputPath, folderRelPath, item.name) : (0, import_node_path5.join)(outputPath, item.name);
1603
+ entries.push({ id: item.id, name: item.name, localPath, mimeType: item.mimeType });
1604
+ }
1605
+ }
1606
+ return entries;
1607
+ }
1608
+ async function downloadFile(fileId, localPath, token) {
1609
+ const url = `${DRIVE_FILES_URL2}/${fileId}?alt=media`;
1610
+ const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
1611
+ if (!response.ok) {
1612
+ throw new Error(`Failed to download ${fileId}: ${response.status}`);
1613
+ }
1614
+ (0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(localPath), { recursive: true });
1615
+ const dest = (0, import_node_fs6.createWriteStream)(localPath);
1616
+ await (0, import_promises4.pipeline)(import_node_stream.Readable.fromWeb(response.body), dest);
1617
+ }
1618
+ function collectLocalFiles(dir, base) {
1619
+ const results = [];
1620
+ if (!(0, import_node_fs6.existsSync)(dir)) return results;
1621
+ for (const entry of (0, import_node_fs6.readdirSync)(dir)) {
1622
+ const fullPath = (0, import_node_path5.join)(dir, entry);
1623
+ const stat = (0, import_node_fs6.statSync)(fullPath);
1624
+ if (stat.isDirectory()) {
1625
+ results.push(...collectLocalFiles(fullPath, base));
1626
+ } else {
1627
+ results.push(fullPath);
1628
+ }
1629
+ }
1630
+ return results;
1631
+ }
1632
+ async function runConcurrent(tasks, concurrency) {
1633
+ const results = [];
1634
+ for (let i = 0; i < tasks.length; i += concurrency) {
1635
+ const batch = tasks.slice(i, i + concurrency).map((t) => t());
1636
+ results.push(...await Promise.all(batch));
1637
+ }
1638
+ return results;
1639
+ }
1640
+ async function syncDriveImages(options) {
1641
+ const {
1642
+ folderId,
1643
+ outputPath,
1644
+ mimeTypes = DEFAULT_IMAGE_MIME_TYPES,
1645
+ recursive = true,
1646
+ folderPattern,
1647
+ credentials,
1648
+ cleanSync = false,
1649
+ concurrency = 3
1650
+ } = options;
1651
+ const token = await getAccessToken2(credentials);
1652
+ (0, import_node_fs6.mkdirSync)(outputPath, { recursive: true });
1653
+ const entries = await collectFiles(
1654
+ folderId,
1655
+ "",
1656
+ outputPath,
1657
+ token,
1658
+ mimeTypes,
1659
+ recursive,
1660
+ folderPattern
1661
+ );
1662
+ const downloaded = [];
1663
+ const skipped = [];
1664
+ const errors = [];
1665
+ const tasks = entries.map((entry) => async () => {
1666
+ if ((0, import_node_fs6.existsSync)(entry.localPath)) {
1667
+ console.log(`[driveImageSync] Skipping (exists): ${entry.localPath}`);
1668
+ skipped.push(entry.localPath);
1669
+ return;
1670
+ }
1671
+ console.log(`[driveImageSync] Downloading: ${entry.localPath}`);
1672
+ try {
1673
+ await downloadFile(entry.id, entry.localPath, token);
1674
+ downloaded.push(entry.localPath);
1675
+ } catch (err) {
1676
+ const msg = err instanceof Error ? err.message : String(err);
1677
+ console.error(`[driveImageSync] Error downloading ${entry.localPath}: ${msg}`);
1678
+ errors.push(entry.localPath);
1679
+ }
1680
+ });
1681
+ await runConcurrent(tasks, concurrency);
1682
+ const deleted = [];
1683
+ if (cleanSync) {
1684
+ const driveLocalPaths = new Set(entries.map((e) => e.localPath));
1685
+ const localFiles = collectLocalFiles(outputPath, outputPath);
1686
+ for (const localFile of localFiles) {
1687
+ if (!driveLocalPaths.has(localFile)) {
1688
+ console.log(`[driveImageSync] Deleting (not in Drive): ${localFile}`);
1689
+ (0, import_node_fs6.unlinkSync)(localFile);
1690
+ deleted.push(localFile);
1691
+ }
1692
+ }
1693
+ }
1694
+ console.log(
1695
+ `[driveImageSync] Synced ${downloaded.length} files, skipped ${skipped.length}, deleted ${deleted.length}, errors ${errors.length}`
1696
+ );
1697
+ return { downloaded, skipped, deleted, errors };
1698
+ }
1699
+
1700
+ // src/utils/getDriveTranslations.ts
1701
+ async function manageDriveTranslations(options) {
1702
+ const {
1703
+ driveFolderId,
1704
+ scanForSpreadsheets = true,
1705
+ spreadsheetIds: explicitIds = [],
1706
+ spreadsheetNameFilter,
1707
+ syncImages = false,
1708
+ imageOutputPath,
1709
+ imageSyncOptions,
1710
+ translationOptions = {},
1711
+ docTitles
1712
+ } = options;
1713
+ if (syncImages && !imageOutputPath) {
1714
+ throw new Error(
1715
+ "[manageDriveTranslations] imageOutputPath is required when syncImages is true"
1716
+ );
1717
+ }
1718
+ const discoveredIds = [];
1719
+ const discoveredNames = /* @__PURE__ */ new Map();
1720
+ if (driveFolderId && scanForSpreadsheets) {
1721
+ const scanOptions = { folderId: driveFolderId };
1722
+ const discovered = await scanDriveFolderForSpreadsheets(scanOptions);
1723
+ console.log(
1724
+ `[manageDriveTranslations] Found ${discovered.length} spreadsheet(s) in Drive folder`
1725
+ );
1726
+ for (const file of discovered) {
1727
+ discoveredIds.push(file.id);
1728
+ discoveredNames.set(file.id, file.name);
1729
+ }
1730
+ }
1731
+ const allIds = [.../* @__PURE__ */ new Set([...discoveredIds, ...explicitIds])];
1732
+ const filteredIds = spreadsheetNameFilter ? allIds.filter((id) => {
1733
+ const name = discoveredNames.get(id);
1734
+ if (!name) return true;
1735
+ return spreadsheetNameFilter.test(name);
1736
+ }) : allIds;
1737
+ const translations = await getMultipleSpreadSheetsData(docTitles, {
1738
+ ...translationOptions,
1739
+ spreadsheetIds: filteredIds.length > 0 ? filteredIds : void 0
1740
+ });
1741
+ let imageSync;
1742
+ if (syncImages && driveFolderId && imageOutputPath) {
1743
+ imageSync = await syncDriveImages({
1744
+ ...imageSyncOptions,
1745
+ folderId: driveFolderId,
1746
+ outputPath: imageOutputPath
1747
+ });
1748
+ }
1749
+ return { translations, spreadsheetIds: filteredIds, imageSync };
1750
+ }
1751
+
1355
1752
  // src/index.ts
1356
1753
  var index_default = getSpreadSheetData;
1357
1754
  // Annotate the CommonJS export names for ESM import in node:
@@ -1364,19 +1761,25 @@ var index_default = getSpreadSheetData;
1364
1761
  createSpreadsheet,
1365
1762
  filterValidLocales,
1366
1763
  findLocalChanges,
1764
+ getGoogleTranslateCode,
1367
1765
  getLanguagePrefix,
1368
1766
  getLocaleDisplayName,
1767
+ getMultipleSpreadSheetsData,
1369
1768
  getNormalizedLocaleForHeader,
1370
1769
  getOriginalHeaderForLocale,
1371
1770
  getSpreadSheetData,
1372
1771
  getTranslationSummary,
1373
1772
  handleBidirectionalSync,
1374
1773
  isValidLocale,
1774
+ manageDriveTranslations,
1775
+ mergeMultipleTranslationData,
1375
1776
  mergeSheets,
1376
1777
  normalizeLocaleCode,
1377
1778
  processRawRows,
1378
1779
  readPublicSheet,
1379
1780
  resolveLocaleWithFallback,
1781
+ scanDriveFolderForSpreadsheets,
1782
+ syncDriveImages,
1380
1783
  updateSpreadsheetWithLocalChanges,
1381
1784
  validateCredentials,
1382
1785
  validateEnv,
@@ -0,0 +1,26 @@
1
+ import type { GoogleEnvVars } from '../types';
2
+ export interface DriveSpreadsheetFile {
3
+ id: string;
4
+ name: string;
5
+ /** Relative path within the Drive folder (e.g., "subproject/translations") */
6
+ folderPath: string;
7
+ mimeType: string;
8
+ modifiedTime?: string;
9
+ }
10
+ export interface ScanDriveFolderOptions {
11
+ /** Google Drive folder ID to scan */
12
+ folderId: string;
13
+ /** Whether to recursively scan subfolders (default: true) */
14
+ recursive?: boolean;
15
+ /** Only return spreadsheets with names matching this pattern (optional) */
16
+ nameFilter?: RegExp;
17
+ /** Google service account credentials (falls back to env vars) */
18
+ credentials?: GoogleEnvVars;
19
+ }
20
+ /**
21
+ * Scans a Google Drive folder for Google Spreadsheet files.
22
+ * Returns all spreadsheets found (recursively by default).
23
+ * Uses Drive API v3 via authenticated fetch (no googleapis package needed).
24
+ */
25
+ export declare function scanDriveFolderForSpreadsheets(options: ScanDriveFolderOptions): Promise<DriveSpreadsheetFile[]>;
26
+ //# sourceMappingURL=driveFolderScanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driveFolderScanner.d.ts","sourceRoot":"","sources":["../../src/utils/driveFolderScanner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,WAAW,CAAC,EAAE,aAAa,CAAC;CAC7B;AA8HD;;;;GAIG;AACH,wBAAsB,8BAA8B,CAClD,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAKjC"}
@@ -0,0 +1,44 @@
1
+ import type { GoogleEnvVars } from '../types';
2
+ export interface DriveImageSyncOptions {
3
+ /** Google Drive root folder ID to sync images from */
4
+ folderId: string;
5
+ /** Local directory to download images into (will be created if missing) */
6
+ outputPath: string;
7
+ /** Only download files matching these MIME types (default: all image types) */
8
+ mimeTypes?: string[];
9
+ /** Whether to recursively sync subfolders (default: true) */
10
+ recursive?: boolean;
11
+ /**
12
+ * Subfolder filter pattern. If provided, only subfolders whose relative path
13
+ * (from the root folderId) matches this pattern will be synced.
14
+ * The pattern is tested against the full relative path, e.g. "projects/icons".
15
+ * Useful for patterns like `/^projects\//` or `/icons$/`.
16
+ */
17
+ folderPattern?: RegExp;
18
+ /** Google service account credentials (falls back to env vars) */
19
+ credentials?: GoogleEnvVars;
20
+ /** If true, delete local files that no longer exist in Drive (default: false) */
21
+ cleanSync?: boolean;
22
+ /** Max concurrent downloads (default: 3) */
23
+ concurrency?: number;
24
+ }
25
+ export interface DriveImageSyncResult {
26
+ downloaded: string[];
27
+ skipped: string[];
28
+ deleted: string[];
29
+ errors: string[];
30
+ }
31
+ /**
32
+ * Syncs images from a Google Drive folder to a local directory.
33
+ * Preserves subfolder structure. Uses Drive API v3 via fetch.
34
+ *
35
+ * @example
36
+ * await syncDriveImages({
37
+ * folderId: 'your-drive-folder-id',
38
+ * outputPath: './src/assets/remote-images',
39
+ * recursive: true,
40
+ * credentials: { GOOGLE_CLIENT_EMAIL: '...', GOOGLE_PRIVATE_KEY: '...', GOOGLE_SPREADSHEET_ID: '' }
41
+ * });
42
+ */
43
+ export declare function syncDriveImages(options: DriveImageSyncOptions): Promise<DriveImageSyncResult>;
44
+ //# sourceMappingURL=driveImageSync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driveImageSync.d.ts","sourceRoot":"","sources":["../../src/utils/driveImageSync.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,MAAM,WAAW,qBAAqB;IACpC,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B,iFAAiF;IACjF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAoLD;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAoE/B"}
@@ -0,0 +1,80 @@
1
+ import type { TranslationData } from '../types';
2
+ import type { MultiSpreadsheetOptions } from '../getMultipleSpreadSheetsData';
3
+ import type { DriveImageSyncOptions, DriveImageSyncResult } from './driveImageSync';
4
+ export interface GoogleDriveManagerOptions {
5
+ /**
6
+ * Google Drive folder ID to scan for spreadsheets and/or images.
7
+ * If provided without explicit spreadsheetIds, the folder is scanned
8
+ * automatically for spreadsheet files.
9
+ */
10
+ driveFolderId?: string;
11
+ /**
12
+ * When true, scans driveFolderId for all Google Spreadsheet files and
13
+ * fetches translations from each. Requires driveFolderId. (default: true when driveFolderId set)
14
+ */
15
+ scanForSpreadsheets?: boolean;
16
+ /**
17
+ * Explicit list of spreadsheet IDs to fetch from.
18
+ * If provided together with driveFolderId + scanForSpreadsheets, the
19
+ * explicit list is merged with the discovered ones (deduped).
20
+ */
21
+ spreadsheetIds?: string[];
22
+ /**
23
+ * Optional filter: only process spreadsheets whose name matches this pattern.
24
+ * Useful when the Drive folder contains non-translation spreadsheets.
25
+ * @example /^translations-/i
26
+ */
27
+ spreadsheetNameFilter?: RegExp;
28
+ /**
29
+ * When true, also sync images from driveFolderId to imageOutputPath.
30
+ * Requires driveFolderId. (default: false)
31
+ */
32
+ syncImages?: boolean;
33
+ /**
34
+ * Local directory to download Drive images into.
35
+ * Required when syncImages: true.
36
+ * @example './src/assets/remote-images'
37
+ */
38
+ imageOutputPath?: string;
39
+ /**
40
+ * Image sync options passed to syncDriveImages (mimeTypes, concurrency, etc.)
41
+ */
42
+ imageSyncOptions?: Partial<DriveImageSyncOptions>;
43
+ /**
44
+ * Options forwarded to getMultipleSpreadSheetsData (rowLimit, waitSeconds,
45
+ * translationsOutputDir, autoTranslate, etc.)
46
+ */
47
+ translationOptions?: MultiSpreadsheetOptions;
48
+ /** Sheet names to fetch from each discovered spreadsheet */
49
+ docTitles?: string[];
50
+ }
51
+ export interface GoogleDriveManagerResult {
52
+ translations: TranslationData;
53
+ /** List of spreadsheet IDs that were processed */
54
+ spreadsheetIds: string[];
55
+ /** Image sync result (only present if syncImages: true) */
56
+ imageSync?: DriveImageSyncResult;
57
+ }
58
+ /**
59
+ * Top-level "headless CMS bridge" function.
60
+ *
61
+ * Scans a Google Drive folder for spreadsheets, fetches all translations,
62
+ * optionally syncs images, and returns merged results.
63
+ *
64
+ * @example
65
+ * const result = await manageDriveTranslations({
66
+ * driveFolderId: 'your-folder-id',
67
+ * scanForSpreadsheets: true,
68
+ * spreadsheetNameFilter: /^i18n-/,
69
+ * syncImages: true,
70
+ * imageOutputPath: './src/assets/remote-images',
71
+ * translationOptions: {
72
+ * autoTranslate: false,
73
+ * translationsOutputDir: './src/translations'
74
+ * }
75
+ * });
76
+ * console.log(result.translations);
77
+ * console.log(result.imageSync?.downloaded.length + ' images downloaded');
78
+ */
79
+ export declare function manageDriveTranslations(options: GoogleDriveManagerOptions): Promise<GoogleDriveManagerResult>;
80
+ //# sourceMappingURL=getDriveTranslations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getDriveTranslations.d.ts","sourceRoot":"","sources":["../../src/utils/getDriveTranslations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAI9E,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAGpF,MAAM,WAAW,yBAAyB;IACzC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAE1B;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAElD;;;OAGG;IACH,kBAAkB,CAAC,EAAE,uBAAuB,CAAC;IAE7C,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACxC,YAAY,EAAE,eAAe,CAAC;IAC9B,kDAAkD;IAClD,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,oBAAoB,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,uBAAuB,CAC5C,OAAO,EAAE,yBAAyB,GAChC,OAAO,CAAC,wBAAwB,CAAC,CAkEnC"}