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

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
package/dist/esm/index.js CHANGED
@@ -4,17 +4,24 @@ import path4 from "node:path";
4
4
  import { GoogleSpreadsheet as GoogleSpreadsheet2 } from "google-spreadsheet";
5
5
 
6
6
  // src/utils/auth.ts
7
- import { JWT } from "google-auth-library";
7
+ import { GoogleAuth } from "google-auth-library";
8
8
 
9
9
  // src/utils/validateEnv.ts
10
10
  function validateCredentials() {
11
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
12
+ return {
13
+ GOOGLE_CLIENT_EMAIL: process.env.GOOGLE_CLIENT_EMAIL ?? "",
14
+ GOOGLE_PRIVATE_KEY: process.env.GOOGLE_PRIVATE_KEY ?? ""
15
+ };
16
+ }
11
17
  const requiredVars = ["GOOGLE_CLIENT_EMAIL", "GOOGLE_PRIVATE_KEY"];
12
18
  const missing = requiredVars.filter((v) => !process.env[v]);
13
19
  if (missing.length > 0) {
14
20
  throw new Error(
15
21
  `Missing required environment variables: ${missing.join(", ")}
16
22
 
17
- Make sure these are set in your .env file or environment.`
23
+ Make sure these are set in your .env file or environment.
24
+ Alternatively, set GOOGLE_APPLICATION_CREDENTIALS for Workload Identity Federation.`
18
25
  );
19
26
  }
20
27
  return {
@@ -23,6 +30,21 @@ Make sure these are set in your .env file or environment.`
23
30
  };
24
31
  }
25
32
  function validateEnv() {
33
+ const spreadsheetId = process.env.GOOGLE_SPREADSHEET_ID;
34
+ if (!spreadsheetId) {
35
+ throw new Error(
36
+ `Missing required environment variable: GOOGLE_SPREADSHEET_ID
37
+
38
+ Make sure this is set in your .env file or environment.`
39
+ );
40
+ }
41
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
42
+ return {
43
+ GOOGLE_CLIENT_EMAIL: process.env.GOOGLE_CLIENT_EMAIL ?? "",
44
+ GOOGLE_PRIVATE_KEY: process.env.GOOGLE_PRIVATE_KEY ?? "",
45
+ GOOGLE_SPREADSHEET_ID: spreadsheetId
46
+ };
47
+ }
26
48
  const requiredVars = [
27
49
  "GOOGLE_CLIENT_EMAIL",
28
50
  "GOOGLE_PRIVATE_KEY",
@@ -33,24 +55,43 @@ function validateEnv() {
33
55
  throw new Error(
34
56
  `Missing required environment variables: ${missingVars.join(", ")}
35
57
 
36
- Make sure these are set in your .env file or environment.`
58
+ Make sure these are set in your .env file or environment.
59
+ Alternatively, set GOOGLE_APPLICATION_CREDENTIALS for Workload Identity Federation.`
37
60
  );
38
61
  }
39
62
  return {
40
63
  GOOGLE_CLIENT_EMAIL: process.env.GOOGLE_CLIENT_EMAIL,
41
64
  GOOGLE_PRIVATE_KEY: process.env.GOOGLE_PRIVATE_KEY,
42
- GOOGLE_SPREADSHEET_ID: process.env.GOOGLE_SPREADSHEET_ID
65
+ GOOGLE_SPREADSHEET_ID: spreadsheetId
43
66
  };
44
67
  }
45
68
 
46
69
  // src/utils/auth.ts
70
+ function normalizePrivateKey(key) {
71
+ let normalized = key;
72
+ const outer = key.trim();
73
+ if (outer.startsWith('"') && outer.endsWith('"') || outer.startsWith("'") && outer.endsWith("'")) {
74
+ normalized = outer.slice(1, -1);
75
+ }
76
+ normalized = normalized.replace(/\\n/g, "\n");
77
+ normalized = normalized.replace(/\r\n/g, "\n");
78
+ return normalized;
79
+ }
80
+ function buildGoogleAuth(scopes, credentials) {
81
+ if (credentials) {
82
+ return new GoogleAuth({ credentials, scopes });
83
+ }
84
+ return new GoogleAuth({ scopes });
85
+ }
47
86
  function createAuthClient() {
87
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
88
+ return buildGoogleAuth(["https://www.googleapis.com/auth/spreadsheets"]);
89
+ }
48
90
  const { GOOGLE_CLIENT_EMAIL, GOOGLE_PRIVATE_KEY } = validateCredentials();
49
- const normalizedKey = GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n");
50
- return new JWT({
51
- email: GOOGLE_CLIENT_EMAIL,
52
- key: normalizedKey,
53
- scopes: ["https://www.googleapis.com/auth/spreadsheets"]
91
+ const normalizedKey = normalizePrivateKey(GOOGLE_PRIVATE_KEY);
92
+ return buildGoogleAuth(["https://www.googleapis.com/auth/spreadsheets"], {
93
+ client_email: GOOGLE_CLIENT_EMAIL,
94
+ private_key: normalizedKey
54
95
  });
55
96
  }
56
97
 
@@ -1037,7 +1078,6 @@ async function createSpreadsheet(authClient, options = {}) {
1037
1078
  targetLocales = DEFAULT_TARGET_LOCALES,
1038
1079
  seedKeys = STARTER_KEYS
1039
1080
  } = options;
1040
- await authClient.authorize();
1041
1081
  const createRes = await withRetry(
1042
1082
  () => authClient.request({
1043
1083
  url: "https://sheets.googleapis.com/v4/spreadsheets",
@@ -1359,23 +1399,22 @@ async function getMultipleSpreadSheetsData(docTitles, options = {}) {
1359
1399
  }
1360
1400
 
1361
1401
  // src/utils/driveFolderScanner.ts
1362
- import { GoogleAuth } from "google-auth-library";
1363
1402
  var SPREADSHEET_MIME = "application/vnd.google-apps.spreadsheet";
1364
1403
  var FOLDER_MIME = "application/vnd.google-apps.folder";
1365
1404
  var DRIVE_FILES_URL = "https://www.googleapis.com/drive/v3/files";
1405
+ var DRIVE_SCOPES = ["https://www.googleapis.com/auth/drive.readonly"];
1366
1406
  async function getAccessToken(credentials) {
1367
1407
  const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1368
1408
  const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
1369
- if (!clientEmail || !privateKey) {
1409
+ let driveCredentials;
1410
+ if (clientEmail && privateKey) {
1411
+ driveCredentials = { client_email: clientEmail, private_key: normalizePrivateKey(privateKey) };
1412
+ } else if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
1370
1413
  throw new Error(
1371
- "Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
1414
+ "Google Drive credentials required: set GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY, or set GOOGLE_APPLICATION_CREDENTIALS for Workload Identity Federation."
1372
1415
  );
1373
1416
  }
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
- });
1417
+ const auth = buildGoogleAuth(DRIVE_SCOPES, driveCredentials);
1379
1418
  const client = await auth.getClient();
1380
1419
  const tokenResponse = await client.getAccessToken();
1381
1420
  return tokenResponse.token;
@@ -1450,7 +1489,6 @@ import { createWriteStream, mkdirSync, existsSync, readdirSync, unlinkSync, stat
1450
1489
  import { join, dirname } from "node:path";
1451
1490
  import { pipeline } from "node:stream/promises";
1452
1491
  import { Readable } from "node:stream";
1453
- import { GoogleAuth as GoogleAuth2 } from "google-auth-library";
1454
1492
  var DEFAULT_IMAGE_MIME_TYPES = [
1455
1493
  "image/jpeg",
1456
1494
  "image/jpg",
@@ -1466,19 +1504,27 @@ var DEFAULT_IMAGE_MIME_TYPES = [
1466
1504
  ];
1467
1505
  var FOLDER_MIME2 = "application/vnd.google-apps.folder";
1468
1506
  var DRIVE_FILES_URL2 = "https://www.googleapis.com/drive/v3/files";
1507
+ function normalizeExtension(name) {
1508
+ const dot = name.lastIndexOf(".");
1509
+ if (dot === -1) return name;
1510
+ const base = name.slice(0, dot);
1511
+ let ext = name.slice(dot + 1).toLowerCase();
1512
+ if (ext === "jpeg") ext = "jpg";
1513
+ return `${base}.${ext}`;
1514
+ }
1515
+ var DRIVE_SCOPES2 = ["https://www.googleapis.com/auth/drive.readonly"];
1469
1516
  async function getAccessToken2(credentials) {
1470
1517
  const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1471
1518
  const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
1472
- if (!clientEmail || !privateKey) {
1519
+ let driveCredentials;
1520
+ if (clientEmail && privateKey) {
1521
+ driveCredentials = { client_email: clientEmail, private_key: normalizePrivateKey(privateKey) };
1522
+ } else if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
1473
1523
  throw new Error(
1474
- "Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
1524
+ "Google Drive credentials required: set GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY, or set GOOGLE_APPLICATION_CREDENTIALS for Workload Identity Federation."
1475
1525
  );
1476
1526
  }
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
- });
1527
+ const auth = buildGoogleAuth(DRIVE_SCOPES2, driveCredentials);
1482
1528
  const client = await auth.getClient();
1483
1529
  const tokenResponse = await client.getAccessToken();
1484
1530
  return tokenResponse.token;
@@ -1491,7 +1537,7 @@ async function listFilesInFolder2(folderId, token, mimeTypeFilter) {
1491
1537
  const query = `'${folderId}' in parents${mimeClause} and trashed = false`;
1492
1538
  const params = new URLSearchParams({
1493
1539
  q: query,
1494
- fields: "nextPageToken,files(id,name,mimeType,parents)",
1540
+ fields: "nextPageToken,files(id,name,mimeType,modifiedTime,parents)",
1495
1541
  pageSize: "1000"
1496
1542
  });
1497
1543
  if (pageToken) params.set("pageToken", pageToken);
@@ -1508,7 +1554,7 @@ async function listFilesInFolder2(folderId, token, mimeTypeFilter) {
1508
1554
  } while (pageToken);
1509
1555
  return results;
1510
1556
  }
1511
- async function collectFiles(folderId, folderRelPath, outputPath, token, allowedMimeTypes, recursive, folderPattern) {
1557
+ async function collectFiles(folderId, folderRelPath, outputPath, token, allowedMimeTypes, recursive, folderPattern, normalizeExts = true) {
1512
1558
  console.log(`[driveImageSync] Scanning folder: ${folderId} (path: "${folderRelPath}")`);
1513
1559
  const allItems = await listFilesInFolder2(folderId, token);
1514
1560
  const entries = [];
@@ -1524,12 +1570,20 @@ async function collectFiles(folderId, folderRelPath, outputPath, token, allowedM
1524
1570
  token,
1525
1571
  allowedMimeTypes,
1526
1572
  recursive,
1527
- folderPattern
1573
+ folderPattern,
1574
+ normalizeExts
1528
1575
  );
1529
1576
  entries.push(...subEntries);
1530
1577
  } 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 });
1578
+ const localName = normalizeExts ? normalizeExtension(item.name) : item.name;
1579
+ const localPath = folderRelPath ? join(outputPath, folderRelPath, localName) : join(outputPath, localName);
1580
+ entries.push({
1581
+ id: item.id,
1582
+ name: item.name,
1583
+ localPath,
1584
+ mimeType: item.mimeType,
1585
+ driveModifiedTime: item.modifiedTime
1586
+ });
1533
1587
  }
1534
1588
  }
1535
1589
  return entries;
@@ -1575,7 +1629,9 @@ async function syncDriveImages(options) {
1575
1629
  folderPattern,
1576
1630
  credentials,
1577
1631
  cleanSync = false,
1578
- concurrency = 3
1632
+ concurrency = 3,
1633
+ incrementalSync = true,
1634
+ normalizeExtensions = true
1579
1635
  } = options;
1580
1636
  const token = await getAccessToken2(credentials);
1581
1637
  mkdirSync(outputPath, { recursive: true });
@@ -1586,16 +1642,33 @@ async function syncDriveImages(options) {
1586
1642
  token,
1587
1643
  mimeTypes,
1588
1644
  recursive,
1589
- folderPattern
1645
+ folderPattern,
1646
+ normalizeExtensions
1590
1647
  );
1591
1648
  const downloaded = [];
1592
1649
  const skipped = [];
1593
1650
  const errors = [];
1594
1651
  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;
1652
+ const localExists = existsSync(entry.localPath);
1653
+ if (localExists) {
1654
+ if (incrementalSync && entry.driveModifiedTime) {
1655
+ try {
1656
+ const localMtimeMs = statSync(entry.localPath).mtimeMs;
1657
+ const driveMtimeMs = new Date(entry.driveModifiedTime).getTime();
1658
+ if (driveMtimeMs <= localMtimeMs) {
1659
+ console.log(`[driveImageSync] Skipping (up to date): ${entry.localPath}`);
1660
+ skipped.push(entry.localPath);
1661
+ return;
1662
+ }
1663
+ console.log(`[driveImageSync] Re-downloading (changed in Drive): ${entry.localPath}`);
1664
+ } catch {
1665
+ console.log(`[driveImageSync] Downloading (could not stat local): ${entry.localPath}`);
1666
+ }
1667
+ } else {
1668
+ console.log(`[driveImageSync] Skipping (exists): ${entry.localPath}`);
1669
+ skipped.push(entry.localPath);
1670
+ return;
1671
+ }
1599
1672
  }
1600
1673
  console.log(`[driveImageSync] Downloading: ${entry.localPath}`);
1601
1674
  try {
@@ -1626,7 +1699,134 @@ async function syncDriveImages(options) {
1626
1699
  return { downloaded, skipped, deleted, errors };
1627
1700
  }
1628
1701
 
1702
+ // src/utils/localImageUtils.ts
1703
+ import { promises as fsp } from "node:fs";
1704
+ import { join as join2, extname } from "node:path";
1705
+ async function walkDirectory(dir, options) {
1706
+ const { extensions } = options ?? {};
1707
+ const lowerExts = extensions?.map((e) => e.toLowerCase());
1708
+ async function go(current) {
1709
+ const entries = await fsp.readdir(current, { withFileTypes: true, encoding: "utf8" });
1710
+ const results = [];
1711
+ for (const entry of entries) {
1712
+ const full = join2(current, entry.name);
1713
+ if (entry.isDirectory()) {
1714
+ results.push(...await go(full));
1715
+ } else if (entry.isFile()) {
1716
+ if (!lowerExts || lowerExts.includes(extname(entry.name).toLowerCase())) {
1717
+ results.push(full);
1718
+ }
1719
+ }
1720
+ }
1721
+ return results;
1722
+ }
1723
+ return go(dir);
1724
+ }
1725
+ var DEFAULT_IMAGE_EXTENSIONS = [
1726
+ ".jpg",
1727
+ ".jpeg",
1728
+ ".png",
1729
+ ".webp",
1730
+ ".avif",
1731
+ ".gif",
1732
+ ".svg",
1733
+ ".tiff",
1734
+ ".bmp",
1735
+ ".ico"
1736
+ ];
1737
+ async function validateImageDirectory(options) {
1738
+ const {
1739
+ rootDir,
1740
+ imageExtensions = DEFAULT_IMAGE_EXTENSIONS,
1741
+ allowRootFiles = false,
1742
+ expectedSubfolders = []
1743
+ } = options;
1744
+ const errors = [];
1745
+ const warnings = [];
1746
+ const rootFiles = [];
1747
+ const subfolders = [];
1748
+ const lowerExts = imageExtensions.map((e) => e.toLowerCase());
1749
+ let entries;
1750
+ try {
1751
+ entries = await fsp.readdir(rootDir, { withFileTypes: true, encoding: "utf8" });
1752
+ } catch (err) {
1753
+ const msg = err instanceof Error ? err.message : String(err);
1754
+ return {
1755
+ valid: false,
1756
+ errors: [`Could not read directory "${rootDir}": ${msg}`],
1757
+ warnings,
1758
+ rootFiles,
1759
+ subfolders
1760
+ };
1761
+ }
1762
+ for (const entry of entries) {
1763
+ if (entry.isDirectory()) {
1764
+ subfolders.push(entry.name);
1765
+ } else if (entry.isFile()) {
1766
+ if (lowerExts.includes(extname(entry.name).toLowerCase())) {
1767
+ rootFiles.push(entry.name);
1768
+ }
1769
+ }
1770
+ }
1771
+ if (!allowRootFiles && rootFiles.length > 0) {
1772
+ errors.push(
1773
+ `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)` : ""}`
1774
+ );
1775
+ }
1776
+ if (!allowRootFiles && subfolders.length === 0) {
1777
+ errors.push(
1778
+ `No sub-directories found in "${rootDir}". Expected a nested folder structure (e.g. projects/, performances/).`
1779
+ );
1780
+ }
1781
+ const missing = expectedSubfolders.filter((name) => !subfolders.includes(name));
1782
+ if (missing.length > 0) {
1783
+ warnings.push(
1784
+ `Some expected sub-folders are absent from "${rootDir}": ${missing.join(", ")}`
1785
+ );
1786
+ }
1787
+ return {
1788
+ valid: errors.length === 0,
1789
+ errors,
1790
+ warnings,
1791
+ rootFiles,
1792
+ subfolders
1793
+ };
1794
+ }
1795
+
1629
1796
  // src/utils/getDriveTranslations.ts
1797
+ import path6 from "node:path";
1798
+
1799
+ // src/utils/driveProjectIndex.ts
1800
+ import fs6 from "node:fs";
1801
+ import path5 from "node:path";
1802
+ function buildManifest(options) {
1803
+ const locales = Object.keys(options.translations).sort();
1804
+ return {
1805
+ version: "1",
1806
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1807
+ projectName: options.projectName,
1808
+ domain: options.domain,
1809
+ locales,
1810
+ defaultLocale: options.defaultLocale,
1811
+ spreadsheets: options.spreadsheets,
1812
+ outputDirectory: options.outputDirectory,
1813
+ flatten: options.flatten,
1814
+ projectMetadata: options.projectMetadata
1815
+ };
1816
+ }
1817
+ function writeManifest(manifest, manifestPath) {
1818
+ const dir = path5.dirname(manifestPath);
1819
+ if (!fs6.existsSync(dir)) {
1820
+ fs6.mkdirSync(dir, { recursive: true });
1821
+ }
1822
+ fs6.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
1823
+ console.log(`[driveProjectIndex] Wrote project manifest \u2192 ${manifestPath}`);
1824
+ }
1825
+
1826
+ // src/utils/getDriveTranslations.ts
1827
+ function sanitizeFolderName(name) {
1828
+ return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "sheet";
1829
+ }
1630
1830
  async function manageDriveTranslations(options) {
1631
1831
  const {
1632
1832
  driveFolderId,
@@ -1637,15 +1837,25 @@ async function manageDriveTranslations(options) {
1637
1837
  imageOutputPath,
1638
1838
  imageSyncOptions,
1639
1839
  translationOptions = {},
1640
- docTitles
1840
+ docTitles,
1841
+ flatten = true,
1842
+ createManifest,
1843
+ manifestPath,
1844
+ projectName,
1845
+ domain,
1846
+ defaultLocale,
1847
+ projectMetadata
1641
1848
  } = options;
1642
1849
  if (syncImages && !imageOutputPath) {
1643
1850
  throw new Error(
1644
1851
  "[manageDriveTranslations] imageOutputPath is required when syncImages is true"
1645
1852
  );
1646
1853
  }
1854
+ const shouldCreateManifest = createManifest ?? driveFolderId !== void 0;
1647
1855
  const discoveredIds = [];
1648
1856
  const discoveredNames = /* @__PURE__ */ new Map();
1857
+ const discoveredFolderPaths = /* @__PURE__ */ new Map();
1858
+ const discoveredModifiedTimes = /* @__PURE__ */ new Map();
1649
1859
  if (driveFolderId && scanForSpreadsheets) {
1650
1860
  const scanOptions = { folderId: driveFolderId };
1651
1861
  const discovered = await scanDriveFolderForSpreadsheets(scanOptions);
@@ -1655,6 +1865,8 @@ async function manageDriveTranslations(options) {
1655
1865
  for (const file of discovered) {
1656
1866
  discoveredIds.push(file.id);
1657
1867
  discoveredNames.set(file.id, file.name);
1868
+ discoveredFolderPaths.set(file.id, file.folderPath);
1869
+ if (file.modifiedTime) discoveredModifiedTimes.set(file.id, file.modifiedTime);
1658
1870
  }
1659
1871
  }
1660
1872
  const allIds = [.../* @__PURE__ */ new Set([...discoveredIds, ...explicitIds])];
@@ -1663,10 +1875,50 @@ async function manageDriveTranslations(options) {
1663
1875
  if (!name) return true;
1664
1876
  return spreadsheetNameFilter.test(name);
1665
1877
  }) : allIds;
1666
- const translations = await getMultipleSpreadSheetsData(docTitles, {
1667
- ...translationOptions,
1668
- spreadsheetIds: filteredIds.length > 0 ? filteredIds : void 0
1669
- });
1878
+ let translations;
1879
+ const spreadsheetEntries = [];
1880
+ const baseOutputDir = translationOptions.translationsOutputDir ?? "translations";
1881
+ if (!flatten) {
1882
+ const { mergeStrategy = "later-wins", ...baseOptions } = translationOptions;
1883
+ const perResults = [];
1884
+ for (const id of filteredIds) {
1885
+ const name = discoveredNames.get(id) ?? id;
1886
+ const subDir = sanitizeFolderName(name);
1887
+ const subOutputDir = path6.join(baseOutputDir, subDir);
1888
+ console.log(
1889
+ `[manageDriveTranslations] (flatten: false) Fetching "${name}" \u2192 ${subOutputDir}`
1890
+ );
1891
+ const result = await getSpreadSheetData(docTitles, {
1892
+ ...baseOptions,
1893
+ spreadsheetId: id,
1894
+ translationsOutputDir: subOutputDir
1895
+ });
1896
+ perResults.push(result);
1897
+ spreadsheetEntries.push({
1898
+ id,
1899
+ name,
1900
+ folderPath: discoveredFolderPaths.get(id) ?? "",
1901
+ sheets: docTitles ?? [],
1902
+ modifiedTime: discoveredModifiedTimes.get(id),
1903
+ outputSubDirectory: subDir
1904
+ });
1905
+ }
1906
+ translations = mergeMultipleTranslationData(perResults, mergeStrategy);
1907
+ } else {
1908
+ translations = await getMultipleSpreadSheetsData(docTitles, {
1909
+ ...translationOptions,
1910
+ spreadsheetIds: filteredIds.length > 0 ? filteredIds : void 0
1911
+ });
1912
+ for (const id of filteredIds) {
1913
+ spreadsheetEntries.push({
1914
+ id,
1915
+ name: discoveredNames.get(id) ?? id,
1916
+ folderPath: discoveredFolderPaths.get(id) ?? "",
1917
+ sheets: docTitles ?? [],
1918
+ modifiedTime: discoveredModifiedTimes.get(id)
1919
+ });
1920
+ }
1921
+ }
1670
1922
  let imageSync;
1671
1923
  if (syncImages && driveFolderId && imageOutputPath) {
1672
1924
  imageSync = await syncDriveImages({
@@ -1675,13 +1927,31 @@ async function manageDriveTranslations(options) {
1675
1927
  outputPath: imageOutputPath
1676
1928
  });
1677
1929
  }
1678
- return { translations, spreadsheetIds: filteredIds, imageSync };
1930
+ let manifest;
1931
+ if (shouldCreateManifest) {
1932
+ const resolvedManifestPath = manifestPath ?? path6.join(baseOutputDir, "i18n-manifest.json");
1933
+ manifest = buildManifest({
1934
+ translations,
1935
+ spreadsheets: spreadsheetEntries,
1936
+ outputDirectory: baseOutputDir,
1937
+ flatten,
1938
+ projectName,
1939
+ domain,
1940
+ defaultLocale,
1941
+ projectMetadata
1942
+ });
1943
+ writeManifest(manifest, resolvedManifestPath);
1944
+ }
1945
+ return { translations, spreadsheetIds: filteredIds, imageSync, manifest };
1679
1946
  }
1680
1947
 
1681
1948
  // src/index.ts
1682
1949
  var index_default = getSpreadSheetData;
1683
1950
  export {
1951
+ DEFAULT_IMAGE_EXTENSIONS,
1684
1952
  DEFAULT_WAIT_SECONDS,
1953
+ buildGoogleAuth,
1954
+ buildManifest,
1685
1955
  convertFromDataJsonFormat,
1686
1956
  convertToDataJsonFormat,
1687
1957
  createAuthClient,
@@ -1703,7 +1973,9 @@ export {
1703
1973
  manageDriveTranslations,
1704
1974
  mergeMultipleTranslationData,
1705
1975
  mergeSheets,
1976
+ normalizeExtension,
1706
1977
  normalizeLocaleCode,
1978
+ normalizePrivateKey,
1707
1979
  processRawRows,
1708
1980
  readPublicSheet,
1709
1981
  resolveLocaleWithFallback,
@@ -1712,9 +1984,12 @@ export {
1712
1984
  updateSpreadsheetWithLocalChanges,
1713
1985
  validateCredentials,
1714
1986
  validateEnv,
1987
+ validateImageDirectory,
1715
1988
  wait,
1989
+ walkDirectory,
1716
1990
  withRetry,
1717
1991
  writeLanguageDataFile,
1718
1992
  writeLocalesFile,
1993
+ writeManifest,
1719
1994
  writeTranslationFiles
1720
1995
  };
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export type { SpreadsheetOptions } from './utils/configurationHandler';
7
7
  export { wait } from './utils/wait';
8
8
  export { withRetry } from './utils/rateLimiter';
9
9
  export { validateEnv } from './utils/validateEnv';
10
- export { createAuthClient } from './utils/auth';
10
+ export { createAuthClient, buildGoogleAuth, normalizePrivateKey } from './utils/auth';
11
11
  export { convertToDataJsonFormat } from './utils/dataConverter/convertToDataJsonFormat';
12
12
  export { convertFromDataJsonFormat } from './utils/dataConverter/convertFromDataJsonFormat';
13
13
  export { findLocalChanges } from './utils/dataConverter/findLocalChanges';
@@ -30,10 +30,14 @@ export type { MultiSpreadsheetOptions } from './getMultipleSpreadSheetsData';
30
30
  export { mergeMultipleTranslationData } from './utils/multiSpreadsheetMerger';
31
31
  export { scanDriveFolderForSpreadsheets } from './utils/driveFolderScanner';
32
32
  export type { DriveSpreadsheetFile, ScanDriveFolderOptions } from './utils/driveFolderScanner';
33
- export { syncDriveImages } from './utils/driveImageSync';
33
+ export { syncDriveImages, normalizeExtension } from './utils/driveImageSync';
34
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';
35
37
  export { manageDriveTranslations } from './utils/getDriveTranslations';
36
38
  export type { GoogleDriveManagerOptions, GoogleDriveManagerResult } from './utils/getDriveTranslations';
39
+ export { buildManifest, writeManifest } from './utils/driveProjectIndex';
40
+ export type { DriveProjectManifest, SpreadsheetManifestEntry, BuildManifestOptions } from './utils/driveProjectIndex';
37
41
  import { getSpreadSheetData } from './getSpreadSheetData';
38
42
  export default getSpreadSheetData;
39
43
  //# 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,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}