@el-j/google-sheet-translations 2.1.5-beta.1 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"action-entrypoint.d.ts","sourceRoot":"","sources":["../src/action-entrypoint.ts"],"names":[],"mappings":"AAKA,wBAAsB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CA+DzC"}
1
+ {"version":3,"file":"action-entrypoint.d.ts","sourceRoot":"","sources":["../src/action-entrypoint.ts"],"names":[],"mappings":"AAMA,wBAAsB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CA2FzC"}
package/dist/esm/index.js CHANGED
@@ -1317,6 +1317,367 @@ function mergeSheets(translations, locale, sheetNames) {
1317
1317
  return merged;
1318
1318
  }
1319
1319
 
1320
+ // src/utils/multiSpreadsheetMerger.ts
1321
+ function mergeMultipleTranslationData(results, mergeStrategy = "later-wins") {
1322
+ const merged = {};
1323
+ for (const result of results) {
1324
+ for (const [locale, sheets] of Object.entries(result)) {
1325
+ if (!merged[locale]) {
1326
+ merged[locale] = {};
1327
+ }
1328
+ for (const [sheet, keys] of Object.entries(sheets)) {
1329
+ if (!merged[locale][sheet]) {
1330
+ merged[locale][sheet] = {};
1331
+ }
1332
+ for (const [key, value] of Object.entries(keys)) {
1333
+ if (mergeStrategy === "first-wins" && key in merged[locale][sheet]) {
1334
+ continue;
1335
+ }
1336
+ merged[locale][sheet][key] = value;
1337
+ }
1338
+ }
1339
+ }
1340
+ }
1341
+ return merged;
1342
+ }
1343
+
1344
+ // src/getMultipleSpreadSheetsData.ts
1345
+ async function getMultipleSpreadSheetsData(docTitles, options = {}) {
1346
+ const { spreadsheetIds, mergeStrategy = "later-wins", ...baseOptions } = options;
1347
+ if (!spreadsheetIds || spreadsheetIds.length === 0) {
1348
+ return getSpreadSheetData(docTitles, baseOptions);
1349
+ }
1350
+ console.log(`[getMultipleSpreadSheetsData] Fetching ${spreadsheetIds.length} spreadsheets...`);
1351
+ const results = [];
1352
+ for (let i = 0; i < spreadsheetIds.length; i++) {
1353
+ const id = spreadsheetIds[i];
1354
+ console.log(`[getMultipleSpreadSheetsData] (${i + 1}/${spreadsheetIds.length}) "${id}"...`);
1355
+ const result = await getSpreadSheetData(docTitles, { ...baseOptions, spreadsheetId: id });
1356
+ results.push(result);
1357
+ }
1358
+ return mergeMultipleTranslationData(results, mergeStrategy);
1359
+ }
1360
+
1361
+ // src/utils/driveFolderScanner.ts
1362
+ import { GoogleAuth } from "google-auth-library";
1363
+ var SPREADSHEET_MIME = "application/vnd.google-apps.spreadsheet";
1364
+ var FOLDER_MIME = "application/vnd.google-apps.folder";
1365
+ var DRIVE_FILES_URL = "https://www.googleapis.com/drive/v3/files";
1366
+ async function getAccessToken(credentials) {
1367
+ const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1368
+ const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
1369
+ if (!clientEmail || !privateKey) {
1370
+ throw new Error(
1371
+ "Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
1372
+ );
1373
+ }
1374
+ const normalizedKey = privateKey.replace(/\\n/g, "\n");
1375
+ const auth = new GoogleAuth({
1376
+ credentials: { client_email: clientEmail, private_key: normalizedKey },
1377
+ scopes: ["https://www.googleapis.com/auth/drive.readonly"]
1378
+ });
1379
+ const client = await auth.getClient();
1380
+ const tokenResponse = await client.getAccessToken();
1381
+ return tokenResponse.token;
1382
+ }
1383
+ async function listFilesInFolder(folderId, mimeType, token) {
1384
+ const results = [];
1385
+ let pageToken;
1386
+ do {
1387
+ const query = `'${folderId}' in parents and mimeType = '${mimeType}' and trashed = false`;
1388
+ const params = new URLSearchParams({
1389
+ q: query,
1390
+ fields: "nextPageToken,files(id,name,mimeType,modifiedTime,parents)",
1391
+ pageSize: "1000"
1392
+ });
1393
+ if (pageToken) params.set("pageToken", pageToken);
1394
+ const response = await fetch(`${DRIVE_FILES_URL}?${params.toString()}`, {
1395
+ headers: { Authorization: `Bearer ${token}` }
1396
+ });
1397
+ if (!response.ok) {
1398
+ const text = await response.text();
1399
+ throw new Error(
1400
+ `Drive API error ${response.status}: ${text}`
1401
+ );
1402
+ }
1403
+ const data = await response.json();
1404
+ results.push(...data.files);
1405
+ pageToken = data.nextPageToken;
1406
+ } while (pageToken);
1407
+ return results;
1408
+ }
1409
+ async function scanFolder(folderId, folderPath, token, recursive, nameFilter, seen = /* @__PURE__ */ new Set()) {
1410
+ console.log(`[driveFolderScanner] Scanning folder: ${folderId} (path: "${folderPath}")`);
1411
+ const spreadsheets = await listFilesInFolder(folderId, SPREADSHEET_MIME, token);
1412
+ const results = [];
1413
+ for (const file of spreadsheets) {
1414
+ if (seen.has(file.id)) continue;
1415
+ seen.add(file.id);
1416
+ if (nameFilter && !nameFilter.test(file.name)) continue;
1417
+ results.push({
1418
+ id: file.id,
1419
+ name: file.name,
1420
+ folderPath,
1421
+ mimeType: file.mimeType,
1422
+ modifiedTime: file.modifiedTime
1423
+ });
1424
+ }
1425
+ if (recursive) {
1426
+ const subfolders = await listFilesInFolder(folderId, FOLDER_MIME, token);
1427
+ for (const folder of subfolders) {
1428
+ const subPath = folderPath ? `${folderPath}/${folder.name}` : folder.name;
1429
+ const subResults = await scanFolder(
1430
+ folder.id,
1431
+ subPath,
1432
+ token,
1433
+ recursive,
1434
+ nameFilter,
1435
+ seen
1436
+ );
1437
+ results.push(...subResults);
1438
+ }
1439
+ }
1440
+ return results;
1441
+ }
1442
+ async function scanDriveFolderForSpreadsheets(options) {
1443
+ const { folderId, recursive = true, nameFilter, credentials } = options;
1444
+ const token = await getAccessToken(credentials);
1445
+ return scanFolder(folderId, "", token, recursive, nameFilter);
1446
+ }
1447
+
1448
+ // src/utils/driveImageSync.ts
1449
+ import { createWriteStream, mkdirSync, existsSync, readdirSync, unlinkSync, statSync } from "node:fs";
1450
+ import { join, dirname } from "node:path";
1451
+ import { pipeline } from "node:stream/promises";
1452
+ import { Readable } from "node:stream";
1453
+ import { GoogleAuth as GoogleAuth2 } from "google-auth-library";
1454
+ var DEFAULT_IMAGE_MIME_TYPES = [
1455
+ "image/jpeg",
1456
+ "image/jpg",
1457
+ "image/png",
1458
+ "image/webp",
1459
+ "image/avif",
1460
+ "image/gif",
1461
+ "image/svg+xml",
1462
+ "image/tiff",
1463
+ "image/bmp",
1464
+ "image/ico",
1465
+ "image/x-icon"
1466
+ ];
1467
+ var FOLDER_MIME2 = "application/vnd.google-apps.folder";
1468
+ var DRIVE_FILES_URL2 = "https://www.googleapis.com/drive/v3/files";
1469
+ async function getAccessToken2(credentials) {
1470
+ const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1471
+ const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
1472
+ if (!clientEmail || !privateKey) {
1473
+ throw new Error(
1474
+ "Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
1475
+ );
1476
+ }
1477
+ const normalizedKey = privateKey.replace(/\\n/g, "\n");
1478
+ const auth = new GoogleAuth2({
1479
+ credentials: { client_email: clientEmail, private_key: normalizedKey },
1480
+ scopes: ["https://www.googleapis.com/auth/drive.readonly"]
1481
+ });
1482
+ const client = await auth.getClient();
1483
+ const tokenResponse = await client.getAccessToken();
1484
+ return tokenResponse.token;
1485
+ }
1486
+ async function listFilesInFolder2(folderId, token, mimeTypeFilter) {
1487
+ const results = [];
1488
+ let pageToken;
1489
+ do {
1490
+ const mimeClause = mimeTypeFilter ? ` and mimeType = '${mimeTypeFilter}'` : "";
1491
+ const query = `'${folderId}' in parents${mimeClause} and trashed = false`;
1492
+ const params = new URLSearchParams({
1493
+ q: query,
1494
+ fields: "nextPageToken,files(id,name,mimeType,parents)",
1495
+ pageSize: "1000"
1496
+ });
1497
+ if (pageToken) params.set("pageToken", pageToken);
1498
+ const response = await fetch(`${DRIVE_FILES_URL2}?${params.toString()}`, {
1499
+ headers: { Authorization: `Bearer ${token}` }
1500
+ });
1501
+ if (!response.ok) {
1502
+ const text = await response.text();
1503
+ throw new Error(`Drive API error ${response.status}: ${text}`);
1504
+ }
1505
+ const data = await response.json();
1506
+ results.push(...data.files);
1507
+ pageToken = data.nextPageToken;
1508
+ } while (pageToken);
1509
+ return results;
1510
+ }
1511
+ async function collectFiles(folderId, folderRelPath, outputPath, token, allowedMimeTypes, recursive, folderPattern) {
1512
+ console.log(`[driveImageSync] Scanning folder: ${folderId} (path: "${folderRelPath}")`);
1513
+ const allItems = await listFilesInFolder2(folderId, token);
1514
+ const entries = [];
1515
+ for (const item of allItems) {
1516
+ if (item.mimeType === FOLDER_MIME2) {
1517
+ if (!recursive) continue;
1518
+ const subRelPath = folderRelPath ? `${folderRelPath}/${item.name}` : item.name;
1519
+ if (folderPattern && !folderPattern.test(subRelPath)) continue;
1520
+ const subEntries = await collectFiles(
1521
+ item.id,
1522
+ subRelPath,
1523
+ outputPath,
1524
+ token,
1525
+ allowedMimeTypes,
1526
+ recursive,
1527
+ folderPattern
1528
+ );
1529
+ entries.push(...subEntries);
1530
+ } else if (allowedMimeTypes.includes(item.mimeType)) {
1531
+ const localPath = folderRelPath ? join(outputPath, folderRelPath, item.name) : join(outputPath, item.name);
1532
+ entries.push({ id: item.id, name: item.name, localPath, mimeType: item.mimeType });
1533
+ }
1534
+ }
1535
+ return entries;
1536
+ }
1537
+ async function downloadFile(fileId, localPath, token) {
1538
+ const url = `${DRIVE_FILES_URL2}/${fileId}?alt=media`;
1539
+ const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
1540
+ if (!response.ok) {
1541
+ throw new Error(`Failed to download ${fileId}: ${response.status}`);
1542
+ }
1543
+ mkdirSync(dirname(localPath), { recursive: true });
1544
+ const dest = createWriteStream(localPath);
1545
+ await pipeline(Readable.fromWeb(response.body), dest);
1546
+ }
1547
+ function collectLocalFiles(dir, base) {
1548
+ const results = [];
1549
+ if (!existsSync(dir)) return results;
1550
+ for (const entry of readdirSync(dir)) {
1551
+ const fullPath = join(dir, entry);
1552
+ const stat = statSync(fullPath);
1553
+ if (stat.isDirectory()) {
1554
+ results.push(...collectLocalFiles(fullPath, base));
1555
+ } else {
1556
+ results.push(fullPath);
1557
+ }
1558
+ }
1559
+ return results;
1560
+ }
1561
+ async function runConcurrent(tasks, concurrency) {
1562
+ const results = [];
1563
+ for (let i = 0; i < tasks.length; i += concurrency) {
1564
+ const batch = tasks.slice(i, i + concurrency).map((t) => t());
1565
+ results.push(...await Promise.all(batch));
1566
+ }
1567
+ return results;
1568
+ }
1569
+ async function syncDriveImages(options) {
1570
+ const {
1571
+ folderId,
1572
+ outputPath,
1573
+ mimeTypes = DEFAULT_IMAGE_MIME_TYPES,
1574
+ recursive = true,
1575
+ folderPattern,
1576
+ credentials,
1577
+ cleanSync = false,
1578
+ concurrency = 3
1579
+ } = options;
1580
+ const token = await getAccessToken2(credentials);
1581
+ mkdirSync(outputPath, { recursive: true });
1582
+ const entries = await collectFiles(
1583
+ folderId,
1584
+ "",
1585
+ outputPath,
1586
+ token,
1587
+ mimeTypes,
1588
+ recursive,
1589
+ folderPattern
1590
+ );
1591
+ const downloaded = [];
1592
+ const skipped = [];
1593
+ const errors = [];
1594
+ const tasks = entries.map((entry) => async () => {
1595
+ if (existsSync(entry.localPath)) {
1596
+ console.log(`[driveImageSync] Skipping (exists): ${entry.localPath}`);
1597
+ skipped.push(entry.localPath);
1598
+ return;
1599
+ }
1600
+ console.log(`[driveImageSync] Downloading: ${entry.localPath}`);
1601
+ try {
1602
+ await downloadFile(entry.id, entry.localPath, token);
1603
+ downloaded.push(entry.localPath);
1604
+ } catch (err) {
1605
+ const msg = err instanceof Error ? err.message : String(err);
1606
+ console.error(`[driveImageSync] Error downloading ${entry.localPath}: ${msg}`);
1607
+ errors.push(entry.localPath);
1608
+ }
1609
+ });
1610
+ await runConcurrent(tasks, concurrency);
1611
+ const deleted = [];
1612
+ if (cleanSync) {
1613
+ const driveLocalPaths = new Set(entries.map((e) => e.localPath));
1614
+ const localFiles = collectLocalFiles(outputPath, outputPath);
1615
+ for (const localFile of localFiles) {
1616
+ if (!driveLocalPaths.has(localFile)) {
1617
+ console.log(`[driveImageSync] Deleting (not in Drive): ${localFile}`);
1618
+ unlinkSync(localFile);
1619
+ deleted.push(localFile);
1620
+ }
1621
+ }
1622
+ }
1623
+ console.log(
1624
+ `[driveImageSync] Synced ${downloaded.length} files, skipped ${skipped.length}, deleted ${deleted.length}, errors ${errors.length}`
1625
+ );
1626
+ return { downloaded, skipped, deleted, errors };
1627
+ }
1628
+
1629
+ // src/utils/getDriveTranslations.ts
1630
+ async function manageDriveTranslations(options) {
1631
+ const {
1632
+ driveFolderId,
1633
+ scanForSpreadsheets = true,
1634
+ spreadsheetIds: explicitIds = [],
1635
+ spreadsheetNameFilter,
1636
+ syncImages = false,
1637
+ imageOutputPath,
1638
+ imageSyncOptions,
1639
+ translationOptions = {},
1640
+ docTitles
1641
+ } = options;
1642
+ if (syncImages && !imageOutputPath) {
1643
+ throw new Error(
1644
+ "[manageDriveTranslations] imageOutputPath is required when syncImages is true"
1645
+ );
1646
+ }
1647
+ const discoveredIds = [];
1648
+ const discoveredNames = /* @__PURE__ */ new Map();
1649
+ if (driveFolderId && scanForSpreadsheets) {
1650
+ const scanOptions = { folderId: driveFolderId };
1651
+ const discovered = await scanDriveFolderForSpreadsheets(scanOptions);
1652
+ console.log(
1653
+ `[manageDriveTranslations] Found ${discovered.length} spreadsheet(s) in Drive folder`
1654
+ );
1655
+ for (const file of discovered) {
1656
+ discoveredIds.push(file.id);
1657
+ discoveredNames.set(file.id, file.name);
1658
+ }
1659
+ }
1660
+ const allIds = [.../* @__PURE__ */ new Set([...discoveredIds, ...explicitIds])];
1661
+ const filteredIds = spreadsheetNameFilter ? allIds.filter((id) => {
1662
+ const name = discoveredNames.get(id);
1663
+ if (!name) return true;
1664
+ return spreadsheetNameFilter.test(name);
1665
+ }) : allIds;
1666
+ const translations = await getMultipleSpreadSheetsData(docTitles, {
1667
+ ...translationOptions,
1668
+ spreadsheetIds: filteredIds.length > 0 ? filteredIds : void 0
1669
+ });
1670
+ let imageSync;
1671
+ if (syncImages && driveFolderId && imageOutputPath) {
1672
+ imageSync = await syncDriveImages({
1673
+ ...imageSyncOptions,
1674
+ folderId: driveFolderId,
1675
+ outputPath: imageOutputPath
1676
+ });
1677
+ }
1678
+ return { translations, spreadsheetIds: filteredIds, imageSync };
1679
+ }
1680
+
1320
1681
  // src/index.ts
1321
1682
  var index_default = getSpreadSheetData;
1322
1683
  export {
@@ -1332,17 +1693,22 @@ export {
1332
1693
  getGoogleTranslateCode,
1333
1694
  getLanguagePrefix,
1334
1695
  getLocaleDisplayName,
1696
+ getMultipleSpreadSheetsData,
1335
1697
  getNormalizedLocaleForHeader,
1336
1698
  getOriginalHeaderForLocale,
1337
1699
  getSpreadSheetData,
1338
1700
  getTranslationSummary,
1339
1701
  handleBidirectionalSync,
1340
1702
  isValidLocale,
1703
+ manageDriveTranslations,
1704
+ mergeMultipleTranslationData,
1341
1705
  mergeSheets,
1342
1706
  normalizeLocaleCode,
1343
1707
  processRawRows,
1344
1708
  readPublicSheet,
1345
1709
  resolveLocaleWithFallback,
1710
+ scanDriveFolderForSpreadsheets,
1711
+ syncDriveImages,
1346
1712
  updateSpreadsheetWithLocalChanges,
1347
1713
  validateCredentials,
1348
1714
  validateEnv,
@@ -0,0 +1,19 @@
1
+ import type { TranslationData } from './types';
2
+ import type { SpreadsheetOptions } from './utils/configurationHandler';
3
+ export interface MultiSpreadsheetOptions extends SpreadsheetOptions {
4
+ /** Array of spreadsheet IDs to fetch from. Overrides spreadsheetId if provided. */
5
+ spreadsheetIds?: string[];
6
+ /**
7
+ * How to merge same-locale same-sheet keys from different spreadsheets.
8
+ * 'later-wins': keys from later spreadsheets override earlier (default)
9
+ * 'first-wins': keep first occurrence of each key
10
+ */
11
+ mergeStrategy?: 'later-wins' | 'first-wins';
12
+ }
13
+ /**
14
+ * Fetches translations from multiple Google Spreadsheets and merges them.
15
+ * When spreadsheetIds is not provided, falls back to options.spreadsheetId
16
+ * or GOOGLE_SPREADSHEET_ID env var (same as getSpreadSheetData).
17
+ */
18
+ export declare function getMultipleSpreadSheetsData(docTitles?: string[], options?: MultiSpreadsheetOptions): Promise<TranslationData>;
19
+ //# sourceMappingURL=getMultipleSpreadSheetsData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getMultipleSpreadSheetsData.d.ts","sourceRoot":"","sources":["../src/getMultipleSpreadSheetsData.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAIvE,MAAM,WAAW,uBAAwB,SAAQ,kBAAkB;IAClE,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;OAIG;IACH,aAAa,CAAC,EAAE,YAAY,GAAG,YAAY,CAAC;CAC5C;AAED;;;;GAIG;AACH,wBAAsB,2BAA2B,CAChD,SAAS,CAAC,EAAE,MAAM,EAAE,EACpB,OAAO,GAAE,uBAA4B,GACnC,OAAO,CAAC,eAAe,CAAC,CAmB1B"}
package/dist/index.d.ts CHANGED
@@ -25,6 +25,15 @@ export { writeTranslationFiles, writeLocalesFile, writeLanguageDataFile } from '
25
25
  export { handleBidirectionalSync } from './utils/syncManager';
26
26
  export type { SyncResult } from './utils/syncManager';
27
27
  export type { TranslationData, TranslationValue, SheetRow, GoogleEnvVars, } from './types';
28
+ export { getMultipleSpreadSheetsData } from './getMultipleSpreadSheetsData';
29
+ export type { MultiSpreadsheetOptions } from './getMultipleSpreadSheetsData';
30
+ export { mergeMultipleTranslationData } from './utils/multiSpreadsheetMerger';
31
+ export { scanDriveFolderForSpreadsheets } from './utils/driveFolderScanner';
32
+ export type { DriveSpreadsheetFile, ScanDriveFolderOptions } from './utils/driveFolderScanner';
33
+ export { syncDriveImages } from './utils/driveImageSync';
34
+ export type { DriveImageSyncOptions, DriveImageSyncResult } from './utils/driveImageSync';
35
+ export { manageDriveTranslations } from './utils/getDriveTranslations';
36
+ export type { GoogleDriveManagerOptions, GoogleDriveManagerResult } from './utils/getDriveTranslations';
28
37
  import { getSpreadSheetData } from './getSpreadSheetData';
29
38
  export default getSpreadSheetData;
30
39
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAGhF,YAAY,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAGvE,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,uBAAuB,EAAE,MAAM,+CAA+C,CAAC;AACxF,OAAO,EAAE,yBAAyB,EAAE,MAAM,iDAAiD,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAC;AAC1E,OAAO,EAAE,iCAAiC,EAAE,MAAM,4BAA4B,CAAC;AAG/E,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAGzE,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,0BAA0B,EAC1B,4BAA4B,EAC5B,yBAAyB,GAC1B,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAGpE,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACtG,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAGnF,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAGpG,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGtD,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,QAAQ,EACR,aAAa,GACd,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAGhF,YAAY,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAGvE,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,uBAAuB,EAAE,MAAM,+CAA+C,CAAC;AACxF,OAAO,EAAE,yBAAyB,EAAE,MAAM,iDAAiD,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAC;AAC1E,OAAO,EAAE,iCAAiC,EAAE,MAAM,4BAA4B,CAAC;AAG/E,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAGzE,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,0BAA0B,EAC1B,4BAA4B,EAC5B,yBAAyB,GAC1B,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAGpE,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACtG,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAGnF,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAGpG,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGtD,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,QAAQ,EACR,aAAa,GACd,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAC5E,YAAY,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAG9E,OAAO,EAAE,8BAA8B,EAAE,MAAM,4BAA4B,CAAC;AAC5E,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAG/F,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAG1F,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,YAAY,EAAE,yBAAyB,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAGxG,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,eAAe,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -42,17 +42,22 @@ __export(index_exports, {
42
42
  getGoogleTranslateCode: () => getGoogleTranslateCode,
43
43
  getLanguagePrefix: () => getLanguagePrefix,
44
44
  getLocaleDisplayName: () => getLocaleDisplayName,
45
+ getMultipleSpreadSheetsData: () => getMultipleSpreadSheetsData,
45
46
  getNormalizedLocaleForHeader: () => getNormalizedLocaleForHeader,
46
47
  getOriginalHeaderForLocale: () => getOriginalHeaderForLocale,
47
48
  getSpreadSheetData: () => getSpreadSheetData,
48
49
  getTranslationSummary: () => getTranslationSummary,
49
50
  handleBidirectionalSync: () => handleBidirectionalSync,
50
51
  isValidLocale: () => isValidLocale,
52
+ manageDriveTranslations: () => manageDriveTranslations,
53
+ mergeMultipleTranslationData: () => mergeMultipleTranslationData,
51
54
  mergeSheets: () => mergeSheets,
52
55
  normalizeLocaleCode: () => normalizeLocaleCode,
53
56
  processRawRows: () => processRawRows,
54
57
  readPublicSheet: () => readPublicSheet,
55
58
  resolveLocaleWithFallback: () => resolveLocaleWithFallback,
59
+ scanDriveFolderForSpreadsheets: () => scanDriveFolderForSpreadsheets,
60
+ syncDriveImages: () => syncDriveImages,
56
61
  updateSpreadsheetWithLocalChanges: () => updateSpreadsheetWithLocalChanges,
57
62
  validateCredentials: () => validateCredentials,
58
63
  validateEnv: () => validateEnv,
@@ -1383,6 +1388,367 @@ function mergeSheets(translations, locale, sheetNames) {
1383
1388
  return merged;
1384
1389
  }
1385
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
+
1386
1752
  // src/index.ts
1387
1753
  var index_default = getSpreadSheetData;
1388
1754
  // Annotate the CommonJS export names for ESM import in node:
@@ -1398,17 +1764,22 @@ var index_default = getSpreadSheetData;
1398
1764
  getGoogleTranslateCode,
1399
1765
  getLanguagePrefix,
1400
1766
  getLocaleDisplayName,
1767
+ getMultipleSpreadSheetsData,
1401
1768
  getNormalizedLocaleForHeader,
1402
1769
  getOriginalHeaderForLocale,
1403
1770
  getSpreadSheetData,
1404
1771
  getTranslationSummary,
1405
1772
  handleBidirectionalSync,
1406
1773
  isValidLocale,
1774
+ manageDriveTranslations,
1775
+ mergeMultipleTranslationData,
1407
1776
  mergeSheets,
1408
1777
  normalizeLocaleCode,
1409
1778
  processRawRows,
1410
1779
  readPublicSheet,
1411
1780
  resolveLocaleWithFallback,
1781
+ scanDriveFolderForSpreadsheets,
1782
+ syncDriveImages,
1412
1783
  updateSpreadsheetWithLocalChanges,
1413
1784
  validateCredentials,
1414
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"}
@@ -0,0 +1,7 @@
1
+ import type { TranslationData } from '../types';
2
+ /**
3
+ * Merges multiple TranslationData results (from different spreadsheets) into one.
4
+ * Sheets/keys from later spreadsheets override earlier ones if collisions occur.
5
+ */
6
+ export declare function mergeMultipleTranslationData(results: TranslationData[], mergeStrategy?: 'later-wins' | 'first-wins'): TranslationData;
7
+ //# sourceMappingURL=multiSpreadsheetMerger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multiSpreadsheetMerger.d.ts","sourceRoot":"","sources":["../../src/utils/multiSpreadsheetMerger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEhD;;;GAGG;AACH,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,eAAe,EAAE,EAC1B,aAAa,GAAE,YAAY,GAAG,YAA2B,GACvD,eAAe,CAuBjB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@el-j/google-sheet-translations",
3
- "version": "2.1.5-beta.1",
3
+ "version": "2.2.0-beta.1",
4
4
  "description": "A package to manage translations stored in Google Spreadsheets",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.js",