@el-j/google-sheet-translations 2.1.5-beta.1 → 2.2.0-beta.2

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/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Google Sheet Translations
2
2
 
3
+ [![CI](https://github.com/el-j/google-sheet-translations/actions/workflows/ci.yml/badge.svg)](https://github.com/el-j/google-sheet-translations/actions/workflows/ci.yml)
4
+ [![Release](https://github.com/el-j/google-sheet-translations/actions/workflows/release.yml/badge.svg)](https://github.com/el-j/google-sheet-translations/actions/workflows/release.yml)
5
+ [![Docs](https://github.com/el-j/google-sheet-translations/actions/workflows/docs.yml/badge.svg)](https://github.com/el-j/google-sheet-translations/actions/workflows/docs.yml)
6
+ [![npm version](https://img.shields.io/npm/v/@el-j/google-sheet-translations.svg)](https://www.npmjs.com/package/@el-j/google-sheet-translations)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+
3
9
  A Node.js package for managing translations stored in Google Sheets.
4
10
 
5
11
  ## Features
@@ -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,10 +1317,602 @@ 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
+ function normalizeExtension(name) {
1470
+ const dot = name.lastIndexOf(".");
1471
+ if (dot === -1) return name;
1472
+ const base = name.slice(0, dot);
1473
+ let ext = name.slice(dot + 1).toLowerCase();
1474
+ if (ext === "jpeg") ext = "jpg";
1475
+ return `${base}.${ext}`;
1476
+ }
1477
+ async function getAccessToken2(credentials) {
1478
+ const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1479
+ const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
1480
+ if (!clientEmail || !privateKey) {
1481
+ throw new Error(
1482
+ "Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
1483
+ );
1484
+ }
1485
+ const normalizedKey = privateKey.replace(/\\n/g, "\n");
1486
+ const auth = new GoogleAuth2({
1487
+ credentials: { client_email: clientEmail, private_key: normalizedKey },
1488
+ scopes: ["https://www.googleapis.com/auth/drive.readonly"]
1489
+ });
1490
+ const client = await auth.getClient();
1491
+ const tokenResponse = await client.getAccessToken();
1492
+ return tokenResponse.token;
1493
+ }
1494
+ async function listFilesInFolder2(folderId, token, mimeTypeFilter) {
1495
+ const results = [];
1496
+ let pageToken;
1497
+ do {
1498
+ const mimeClause = mimeTypeFilter ? ` and mimeType = '${mimeTypeFilter}'` : "";
1499
+ const query = `'${folderId}' in parents${mimeClause} and trashed = false`;
1500
+ const params = new URLSearchParams({
1501
+ q: query,
1502
+ fields: "nextPageToken,files(id,name,mimeType,modifiedTime,parents)",
1503
+ pageSize: "1000"
1504
+ });
1505
+ if (pageToken) params.set("pageToken", pageToken);
1506
+ const response = await fetch(`${DRIVE_FILES_URL2}?${params.toString()}`, {
1507
+ headers: { Authorization: `Bearer ${token}` }
1508
+ });
1509
+ if (!response.ok) {
1510
+ const text = await response.text();
1511
+ throw new Error(`Drive API error ${response.status}: ${text}`);
1512
+ }
1513
+ const data = await response.json();
1514
+ results.push(...data.files);
1515
+ pageToken = data.nextPageToken;
1516
+ } while (pageToken);
1517
+ return results;
1518
+ }
1519
+ async function collectFiles(folderId, folderRelPath, outputPath, token, allowedMimeTypes, recursive, folderPattern, normalizeExts = true) {
1520
+ console.log(`[driveImageSync] Scanning folder: ${folderId} (path: "${folderRelPath}")`);
1521
+ const allItems = await listFilesInFolder2(folderId, token);
1522
+ const entries = [];
1523
+ for (const item of allItems) {
1524
+ if (item.mimeType === FOLDER_MIME2) {
1525
+ if (!recursive) continue;
1526
+ const subRelPath = folderRelPath ? `${folderRelPath}/${item.name}` : item.name;
1527
+ if (folderPattern && !folderPattern.test(subRelPath)) continue;
1528
+ const subEntries = await collectFiles(
1529
+ item.id,
1530
+ subRelPath,
1531
+ outputPath,
1532
+ token,
1533
+ allowedMimeTypes,
1534
+ recursive,
1535
+ folderPattern,
1536
+ normalizeExts
1537
+ );
1538
+ entries.push(...subEntries);
1539
+ } else if (allowedMimeTypes.includes(item.mimeType)) {
1540
+ const localName = normalizeExts ? normalizeExtension(item.name) : item.name;
1541
+ const localPath = folderRelPath ? join(outputPath, folderRelPath, localName) : join(outputPath, localName);
1542
+ entries.push({
1543
+ id: item.id,
1544
+ name: item.name,
1545
+ localPath,
1546
+ mimeType: item.mimeType,
1547
+ driveModifiedTime: item.modifiedTime
1548
+ });
1549
+ }
1550
+ }
1551
+ return entries;
1552
+ }
1553
+ async function downloadFile(fileId, localPath, token) {
1554
+ const url = `${DRIVE_FILES_URL2}/${fileId}?alt=media`;
1555
+ const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
1556
+ if (!response.ok) {
1557
+ throw new Error(`Failed to download ${fileId}: ${response.status}`);
1558
+ }
1559
+ mkdirSync(dirname(localPath), { recursive: true });
1560
+ const dest = createWriteStream(localPath);
1561
+ await pipeline(Readable.fromWeb(response.body), dest);
1562
+ }
1563
+ function collectLocalFiles(dir, base) {
1564
+ const results = [];
1565
+ if (!existsSync(dir)) return results;
1566
+ for (const entry of readdirSync(dir)) {
1567
+ const fullPath = join(dir, entry);
1568
+ const stat = statSync(fullPath);
1569
+ if (stat.isDirectory()) {
1570
+ results.push(...collectLocalFiles(fullPath, base));
1571
+ } else {
1572
+ results.push(fullPath);
1573
+ }
1574
+ }
1575
+ return results;
1576
+ }
1577
+ async function runConcurrent(tasks, concurrency) {
1578
+ const results = [];
1579
+ for (let i = 0; i < tasks.length; i += concurrency) {
1580
+ const batch = tasks.slice(i, i + concurrency).map((t) => t());
1581
+ results.push(...await Promise.all(batch));
1582
+ }
1583
+ return results;
1584
+ }
1585
+ async function syncDriveImages(options) {
1586
+ const {
1587
+ folderId,
1588
+ outputPath,
1589
+ mimeTypes = DEFAULT_IMAGE_MIME_TYPES,
1590
+ recursive = true,
1591
+ folderPattern,
1592
+ credentials,
1593
+ cleanSync = false,
1594
+ concurrency = 3,
1595
+ incrementalSync = true,
1596
+ normalizeExtensions = true
1597
+ } = options;
1598
+ const token = await getAccessToken2(credentials);
1599
+ mkdirSync(outputPath, { recursive: true });
1600
+ const entries = await collectFiles(
1601
+ folderId,
1602
+ "",
1603
+ outputPath,
1604
+ token,
1605
+ mimeTypes,
1606
+ recursive,
1607
+ folderPattern,
1608
+ normalizeExtensions
1609
+ );
1610
+ const downloaded = [];
1611
+ const skipped = [];
1612
+ const errors = [];
1613
+ const tasks = entries.map((entry) => async () => {
1614
+ const localExists = existsSync(entry.localPath);
1615
+ if (localExists) {
1616
+ if (incrementalSync && entry.driveModifiedTime) {
1617
+ try {
1618
+ const localMtimeMs = statSync(entry.localPath).mtimeMs;
1619
+ const driveMtimeMs = new Date(entry.driveModifiedTime).getTime();
1620
+ if (driveMtimeMs <= localMtimeMs) {
1621
+ console.log(`[driveImageSync] Skipping (up to date): ${entry.localPath}`);
1622
+ skipped.push(entry.localPath);
1623
+ return;
1624
+ }
1625
+ console.log(`[driveImageSync] Re-downloading (changed in Drive): ${entry.localPath}`);
1626
+ } catch {
1627
+ console.log(`[driveImageSync] Downloading (could not stat local): ${entry.localPath}`);
1628
+ }
1629
+ } else {
1630
+ console.log(`[driveImageSync] Skipping (exists): ${entry.localPath}`);
1631
+ skipped.push(entry.localPath);
1632
+ return;
1633
+ }
1634
+ }
1635
+ console.log(`[driveImageSync] Downloading: ${entry.localPath}`);
1636
+ try {
1637
+ await downloadFile(entry.id, entry.localPath, token);
1638
+ downloaded.push(entry.localPath);
1639
+ } catch (err) {
1640
+ const msg = err instanceof Error ? err.message : String(err);
1641
+ console.error(`[driveImageSync] Error downloading ${entry.localPath}: ${msg}`);
1642
+ errors.push(entry.localPath);
1643
+ }
1644
+ });
1645
+ await runConcurrent(tasks, concurrency);
1646
+ const deleted = [];
1647
+ if (cleanSync) {
1648
+ const driveLocalPaths = new Set(entries.map((e) => e.localPath));
1649
+ const localFiles = collectLocalFiles(outputPath, outputPath);
1650
+ for (const localFile of localFiles) {
1651
+ if (!driveLocalPaths.has(localFile)) {
1652
+ console.log(`[driveImageSync] Deleting (not in Drive): ${localFile}`);
1653
+ unlinkSync(localFile);
1654
+ deleted.push(localFile);
1655
+ }
1656
+ }
1657
+ }
1658
+ console.log(
1659
+ `[driveImageSync] Synced ${downloaded.length} files, skipped ${skipped.length}, deleted ${deleted.length}, errors ${errors.length}`
1660
+ );
1661
+ return { downloaded, skipped, deleted, errors };
1662
+ }
1663
+
1664
+ // src/utils/localImageUtils.ts
1665
+ import { promises as fsp } from "node:fs";
1666
+ import { join as join2, extname } from "node:path";
1667
+ async function walkDirectory(dir, options) {
1668
+ const { extensions } = options ?? {};
1669
+ const lowerExts = extensions?.map((e) => e.toLowerCase());
1670
+ async function go(current) {
1671
+ const entries = await fsp.readdir(current, { withFileTypes: true, encoding: "utf8" });
1672
+ const results = [];
1673
+ for (const entry of entries) {
1674
+ const full = join2(current, entry.name);
1675
+ if (entry.isDirectory()) {
1676
+ results.push(...await go(full));
1677
+ } else if (entry.isFile()) {
1678
+ if (!lowerExts || lowerExts.includes(extname(entry.name).toLowerCase())) {
1679
+ results.push(full);
1680
+ }
1681
+ }
1682
+ }
1683
+ return results;
1684
+ }
1685
+ return go(dir);
1686
+ }
1687
+ var DEFAULT_IMAGE_EXTENSIONS = [
1688
+ ".jpg",
1689
+ ".jpeg",
1690
+ ".png",
1691
+ ".webp",
1692
+ ".avif",
1693
+ ".gif",
1694
+ ".svg",
1695
+ ".tiff",
1696
+ ".bmp",
1697
+ ".ico"
1698
+ ];
1699
+ async function validateImageDirectory(options) {
1700
+ const {
1701
+ rootDir,
1702
+ imageExtensions = DEFAULT_IMAGE_EXTENSIONS,
1703
+ allowRootFiles = false,
1704
+ expectedSubfolders = []
1705
+ } = options;
1706
+ const errors = [];
1707
+ const warnings = [];
1708
+ const rootFiles = [];
1709
+ const subfolders = [];
1710
+ const lowerExts = imageExtensions.map((e) => e.toLowerCase());
1711
+ let entries;
1712
+ try {
1713
+ entries = await fsp.readdir(rootDir, { withFileTypes: true, encoding: "utf8" });
1714
+ } catch (err) {
1715
+ const msg = err instanceof Error ? err.message : String(err);
1716
+ return {
1717
+ valid: false,
1718
+ errors: [`Could not read directory "${rootDir}": ${msg}`],
1719
+ warnings,
1720
+ rootFiles,
1721
+ subfolders
1722
+ };
1723
+ }
1724
+ for (const entry of entries) {
1725
+ if (entry.isDirectory()) {
1726
+ subfolders.push(entry.name);
1727
+ } else if (entry.isFile()) {
1728
+ if (lowerExts.includes(extname(entry.name).toLowerCase())) {
1729
+ rootFiles.push(entry.name);
1730
+ }
1731
+ }
1732
+ }
1733
+ if (!allowRootFiles && rootFiles.length > 0) {
1734
+ errors.push(
1735
+ `Image files found directly in "${rootDir}" \u2014 the folder structure may have been flattened during sync. Files: ${rootFiles.slice(0, 5).join(", ")}${rootFiles.length > 5 ? ` \u2026 (+${rootFiles.length - 5} more)` : ""}`
1736
+ );
1737
+ }
1738
+ if (!allowRootFiles && subfolders.length === 0) {
1739
+ errors.push(
1740
+ `No sub-directories found in "${rootDir}". Expected a nested folder structure (e.g. projects/, performances/).`
1741
+ );
1742
+ }
1743
+ const missing = expectedSubfolders.filter((name) => !subfolders.includes(name));
1744
+ if (missing.length > 0) {
1745
+ warnings.push(
1746
+ `Some expected sub-folders are absent from "${rootDir}": ${missing.join(", ")}`
1747
+ );
1748
+ }
1749
+ return {
1750
+ valid: errors.length === 0,
1751
+ errors,
1752
+ warnings,
1753
+ rootFiles,
1754
+ subfolders
1755
+ };
1756
+ }
1757
+
1758
+ // src/utils/getDriveTranslations.ts
1759
+ import path6 from "node:path";
1760
+
1761
+ // src/utils/driveProjectIndex.ts
1762
+ import fs6 from "node:fs";
1763
+ import path5 from "node:path";
1764
+ function buildManifest(options) {
1765
+ const locales = Object.keys(options.translations).sort();
1766
+ return {
1767
+ version: "1",
1768
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1769
+ projectName: options.projectName,
1770
+ domain: options.domain,
1771
+ locales,
1772
+ defaultLocale: options.defaultLocale,
1773
+ spreadsheets: options.spreadsheets,
1774
+ outputDirectory: options.outputDirectory,
1775
+ flatten: options.flatten,
1776
+ projectMetadata: options.projectMetadata
1777
+ };
1778
+ }
1779
+ function writeManifest(manifest, manifestPath) {
1780
+ const dir = path5.dirname(manifestPath);
1781
+ if (!fs6.existsSync(dir)) {
1782
+ fs6.mkdirSync(dir, { recursive: true });
1783
+ }
1784
+ fs6.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
1785
+ console.log(`[driveProjectIndex] Wrote project manifest \u2192 ${manifestPath}`);
1786
+ }
1787
+
1788
+ // src/utils/getDriveTranslations.ts
1789
+ function sanitizeFolderName(name) {
1790
+ return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "sheet";
1791
+ }
1792
+ async function manageDriveTranslations(options) {
1793
+ const {
1794
+ driveFolderId,
1795
+ scanForSpreadsheets = true,
1796
+ spreadsheetIds: explicitIds = [],
1797
+ spreadsheetNameFilter,
1798
+ syncImages = false,
1799
+ imageOutputPath,
1800
+ imageSyncOptions,
1801
+ translationOptions = {},
1802
+ docTitles,
1803
+ flatten = true,
1804
+ createManifest,
1805
+ manifestPath,
1806
+ projectName,
1807
+ domain,
1808
+ defaultLocale,
1809
+ projectMetadata
1810
+ } = options;
1811
+ if (syncImages && !imageOutputPath) {
1812
+ throw new Error(
1813
+ "[manageDriveTranslations] imageOutputPath is required when syncImages is true"
1814
+ );
1815
+ }
1816
+ const shouldCreateManifest = createManifest ?? driveFolderId !== void 0;
1817
+ const discoveredIds = [];
1818
+ const discoveredNames = /* @__PURE__ */ new Map();
1819
+ const discoveredFolderPaths = /* @__PURE__ */ new Map();
1820
+ const discoveredModifiedTimes = /* @__PURE__ */ new Map();
1821
+ if (driveFolderId && scanForSpreadsheets) {
1822
+ const scanOptions = { folderId: driveFolderId };
1823
+ const discovered = await scanDriveFolderForSpreadsheets(scanOptions);
1824
+ console.log(
1825
+ `[manageDriveTranslations] Found ${discovered.length} spreadsheet(s) in Drive folder`
1826
+ );
1827
+ for (const file of discovered) {
1828
+ discoveredIds.push(file.id);
1829
+ discoveredNames.set(file.id, file.name);
1830
+ discoveredFolderPaths.set(file.id, file.folderPath);
1831
+ if (file.modifiedTime) discoveredModifiedTimes.set(file.id, file.modifiedTime);
1832
+ }
1833
+ }
1834
+ const allIds = [.../* @__PURE__ */ new Set([...discoveredIds, ...explicitIds])];
1835
+ const filteredIds = spreadsheetNameFilter ? allIds.filter((id) => {
1836
+ const name = discoveredNames.get(id);
1837
+ if (!name) return true;
1838
+ return spreadsheetNameFilter.test(name);
1839
+ }) : allIds;
1840
+ let translations;
1841
+ const spreadsheetEntries = [];
1842
+ const baseOutputDir = translationOptions.translationsOutputDir ?? "translations";
1843
+ if (!flatten) {
1844
+ const { mergeStrategy = "later-wins", ...baseOptions } = translationOptions;
1845
+ const perResults = [];
1846
+ for (const id of filteredIds) {
1847
+ const name = discoveredNames.get(id) ?? id;
1848
+ const subDir = sanitizeFolderName(name);
1849
+ const subOutputDir = path6.join(baseOutputDir, subDir);
1850
+ console.log(
1851
+ `[manageDriveTranslations] (flatten: false) Fetching "${name}" \u2192 ${subOutputDir}`
1852
+ );
1853
+ const result = await getSpreadSheetData(docTitles, {
1854
+ ...baseOptions,
1855
+ spreadsheetId: id,
1856
+ translationsOutputDir: subOutputDir
1857
+ });
1858
+ perResults.push(result);
1859
+ spreadsheetEntries.push({
1860
+ id,
1861
+ name,
1862
+ folderPath: discoveredFolderPaths.get(id) ?? "",
1863
+ sheets: docTitles ?? [],
1864
+ modifiedTime: discoveredModifiedTimes.get(id),
1865
+ outputSubDirectory: subDir
1866
+ });
1867
+ }
1868
+ translations = mergeMultipleTranslationData(perResults, mergeStrategy);
1869
+ } else {
1870
+ translations = await getMultipleSpreadSheetsData(docTitles, {
1871
+ ...translationOptions,
1872
+ spreadsheetIds: filteredIds.length > 0 ? filteredIds : void 0
1873
+ });
1874
+ for (const id of filteredIds) {
1875
+ spreadsheetEntries.push({
1876
+ id,
1877
+ name: discoveredNames.get(id) ?? id,
1878
+ folderPath: discoveredFolderPaths.get(id) ?? "",
1879
+ sheets: docTitles ?? [],
1880
+ modifiedTime: discoveredModifiedTimes.get(id)
1881
+ });
1882
+ }
1883
+ }
1884
+ let imageSync;
1885
+ if (syncImages && driveFolderId && imageOutputPath) {
1886
+ imageSync = await syncDriveImages({
1887
+ ...imageSyncOptions,
1888
+ folderId: driveFolderId,
1889
+ outputPath: imageOutputPath
1890
+ });
1891
+ }
1892
+ let manifest;
1893
+ if (shouldCreateManifest) {
1894
+ const resolvedManifestPath = manifestPath ?? path6.join(baseOutputDir, "i18n-manifest.json");
1895
+ manifest = buildManifest({
1896
+ translations,
1897
+ spreadsheets: spreadsheetEntries,
1898
+ outputDirectory: baseOutputDir,
1899
+ flatten,
1900
+ projectName,
1901
+ domain,
1902
+ defaultLocale,
1903
+ projectMetadata
1904
+ });
1905
+ writeManifest(manifest, resolvedManifestPath);
1906
+ }
1907
+ return { translations, spreadsheetIds: filteredIds, imageSync, manifest };
1908
+ }
1909
+
1320
1910
  // src/index.ts
1321
1911
  var index_default = getSpreadSheetData;
1322
1912
  export {
1913
+ DEFAULT_IMAGE_EXTENSIONS,
1323
1914
  DEFAULT_WAIT_SECONDS,
1915
+ buildManifest,
1324
1916
  convertFromDataJsonFormat,
1325
1917
  convertToDataJsonFormat,
1326
1918
  createAuthClient,
@@ -1332,23 +1924,32 @@ export {
1332
1924
  getGoogleTranslateCode,
1333
1925
  getLanguagePrefix,
1334
1926
  getLocaleDisplayName,
1927
+ getMultipleSpreadSheetsData,
1335
1928
  getNormalizedLocaleForHeader,
1336
1929
  getOriginalHeaderForLocale,
1337
1930
  getSpreadSheetData,
1338
1931
  getTranslationSummary,
1339
1932
  handleBidirectionalSync,
1340
1933
  isValidLocale,
1934
+ manageDriveTranslations,
1935
+ mergeMultipleTranslationData,
1341
1936
  mergeSheets,
1937
+ normalizeExtension,
1342
1938
  normalizeLocaleCode,
1343
1939
  processRawRows,
1344
1940
  readPublicSheet,
1345
1941
  resolveLocaleWithFallback,
1942
+ scanDriveFolderForSpreadsheets,
1943
+ syncDriveImages,
1346
1944
  updateSpreadsheetWithLocalChanges,
1347
1945
  validateCredentials,
1348
1946
  validateEnv,
1947
+ validateImageDirectory,
1349
1948
  wait,
1949
+ walkDirectory,
1350
1950
  withRetry,
1351
1951
  writeLanguageDataFile,
1352
1952
  writeLocalesFile,
1953
+ writeManifest,
1353
1954
  writeTranslationFiles
1354
1955
  };
@@ -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,19 @@ 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, normalizeExtension } from './utils/driveImageSync';
34
+ export type { DriveImageSyncOptions, DriveImageSyncResult } from './utils/driveImageSync';
35
+ export { walkDirectory, validateImageDirectory, DEFAULT_IMAGE_EXTENSIONS } from './utils/localImageUtils';
36
+ export type { WalkDirectoryOptions, ImageDirectoryValidationOptions, ImageDirectoryValidationResult, } from './utils/localImageUtils';
37
+ export { manageDriveTranslations } from './utils/getDriveTranslations';
38
+ export type { GoogleDriveManagerOptions, GoogleDriveManagerResult } from './utils/getDriveTranslations';
39
+ export { buildManifest, writeManifest } from './utils/driveProjectIndex';
40
+ export type { DriveProjectManifest, SpreadsheetManifestEntry, BuildManifestOptions } from './utils/driveProjectIndex';
28
41
  import { getSpreadSheetData } from './getSpreadSheetData';
29
42
  export default getSpreadSheetData;
30
43
  //# sourceMappingURL=index.d.ts.map