@el-j/google-sheet-translations 2.2.0-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
package/dist/esm/index.js CHANGED
@@ -1466,6 +1466,14 @@ var DEFAULT_IMAGE_MIME_TYPES = [
1466
1466
  ];
1467
1467
  var FOLDER_MIME2 = "application/vnd.google-apps.folder";
1468
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
+ }
1469
1477
  async function getAccessToken2(credentials) {
1470
1478
  const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1471
1479
  const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
@@ -1491,7 +1499,7 @@ async function listFilesInFolder2(folderId, token, mimeTypeFilter) {
1491
1499
  const query = `'${folderId}' in parents${mimeClause} and trashed = false`;
1492
1500
  const params = new URLSearchParams({
1493
1501
  q: query,
1494
- fields: "nextPageToken,files(id,name,mimeType,parents)",
1502
+ fields: "nextPageToken,files(id,name,mimeType,modifiedTime,parents)",
1495
1503
  pageSize: "1000"
1496
1504
  });
1497
1505
  if (pageToken) params.set("pageToken", pageToken);
@@ -1508,7 +1516,7 @@ async function listFilesInFolder2(folderId, token, mimeTypeFilter) {
1508
1516
  } while (pageToken);
1509
1517
  return results;
1510
1518
  }
1511
- async function collectFiles(folderId, folderRelPath, outputPath, token, allowedMimeTypes, recursive, folderPattern) {
1519
+ async function collectFiles(folderId, folderRelPath, outputPath, token, allowedMimeTypes, recursive, folderPattern, normalizeExts = true) {
1512
1520
  console.log(`[driveImageSync] Scanning folder: ${folderId} (path: "${folderRelPath}")`);
1513
1521
  const allItems = await listFilesInFolder2(folderId, token);
1514
1522
  const entries = [];
@@ -1524,12 +1532,20 @@ async function collectFiles(folderId, folderRelPath, outputPath, token, allowedM
1524
1532
  token,
1525
1533
  allowedMimeTypes,
1526
1534
  recursive,
1527
- folderPattern
1535
+ folderPattern,
1536
+ normalizeExts
1528
1537
  );
1529
1538
  entries.push(...subEntries);
1530
1539
  } 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 });
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
+ });
1533
1549
  }
1534
1550
  }
1535
1551
  return entries;
@@ -1575,7 +1591,9 @@ async function syncDriveImages(options) {
1575
1591
  folderPattern,
1576
1592
  credentials,
1577
1593
  cleanSync = false,
1578
- concurrency = 3
1594
+ concurrency = 3,
1595
+ incrementalSync = true,
1596
+ normalizeExtensions = true
1579
1597
  } = options;
1580
1598
  const token = await getAccessToken2(credentials);
1581
1599
  mkdirSync(outputPath, { recursive: true });
@@ -1586,16 +1604,33 @@ async function syncDriveImages(options) {
1586
1604
  token,
1587
1605
  mimeTypes,
1588
1606
  recursive,
1589
- folderPattern
1607
+ folderPattern,
1608
+ normalizeExtensions
1590
1609
  );
1591
1610
  const downloaded = [];
1592
1611
  const skipped = [];
1593
1612
  const errors = [];
1594
1613
  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;
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
+ }
1599
1634
  }
1600
1635
  console.log(`[driveImageSync] Downloading: ${entry.localPath}`);
1601
1636
  try {
@@ -1626,7 +1661,134 @@ async function syncDriveImages(options) {
1626
1661
  return { downloaded, skipped, deleted, errors };
1627
1662
  }
1628
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
+
1629
1788
  // src/utils/getDriveTranslations.ts
1789
+ function sanitizeFolderName(name) {
1790
+ return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "sheet";
1791
+ }
1630
1792
  async function manageDriveTranslations(options) {
1631
1793
  const {
1632
1794
  driveFolderId,
@@ -1637,15 +1799,25 @@ async function manageDriveTranslations(options) {
1637
1799
  imageOutputPath,
1638
1800
  imageSyncOptions,
1639
1801
  translationOptions = {},
1640
- docTitles
1802
+ docTitles,
1803
+ flatten = true,
1804
+ createManifest,
1805
+ manifestPath,
1806
+ projectName,
1807
+ domain,
1808
+ defaultLocale,
1809
+ projectMetadata
1641
1810
  } = options;
1642
1811
  if (syncImages && !imageOutputPath) {
1643
1812
  throw new Error(
1644
1813
  "[manageDriveTranslations] imageOutputPath is required when syncImages is true"
1645
1814
  );
1646
1815
  }
1816
+ const shouldCreateManifest = createManifest ?? driveFolderId !== void 0;
1647
1817
  const discoveredIds = [];
1648
1818
  const discoveredNames = /* @__PURE__ */ new Map();
1819
+ const discoveredFolderPaths = /* @__PURE__ */ new Map();
1820
+ const discoveredModifiedTimes = /* @__PURE__ */ new Map();
1649
1821
  if (driveFolderId && scanForSpreadsheets) {
1650
1822
  const scanOptions = { folderId: driveFolderId };
1651
1823
  const discovered = await scanDriveFolderForSpreadsheets(scanOptions);
@@ -1655,6 +1827,8 @@ async function manageDriveTranslations(options) {
1655
1827
  for (const file of discovered) {
1656
1828
  discoveredIds.push(file.id);
1657
1829
  discoveredNames.set(file.id, file.name);
1830
+ discoveredFolderPaths.set(file.id, file.folderPath);
1831
+ if (file.modifiedTime) discoveredModifiedTimes.set(file.id, file.modifiedTime);
1658
1832
  }
1659
1833
  }
1660
1834
  const allIds = [.../* @__PURE__ */ new Set([...discoveredIds, ...explicitIds])];
@@ -1663,10 +1837,50 @@ async function manageDriveTranslations(options) {
1663
1837
  if (!name) return true;
1664
1838
  return spreadsheetNameFilter.test(name);
1665
1839
  }) : allIds;
1666
- const translations = await getMultipleSpreadSheetsData(docTitles, {
1667
- ...translationOptions,
1668
- spreadsheetIds: filteredIds.length > 0 ? filteredIds : void 0
1669
- });
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
+ }
1670
1884
  let imageSync;
1671
1885
  if (syncImages && driveFolderId && imageOutputPath) {
1672
1886
  imageSync = await syncDriveImages({
@@ -1675,13 +1889,30 @@ async function manageDriveTranslations(options) {
1675
1889
  outputPath: imageOutputPath
1676
1890
  });
1677
1891
  }
1678
- return { translations, spreadsheetIds: filteredIds, imageSync };
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 };
1679
1908
  }
1680
1909
 
1681
1910
  // src/index.ts
1682
1911
  var index_default = getSpreadSheetData;
1683
1912
  export {
1913
+ DEFAULT_IMAGE_EXTENSIONS,
1684
1914
  DEFAULT_WAIT_SECONDS,
1915
+ buildManifest,
1685
1916
  convertFromDataJsonFormat,
1686
1917
  convertToDataJsonFormat,
1687
1918
  createAuthClient,
@@ -1703,6 +1934,7 @@ export {
1703
1934
  manageDriveTranslations,
1704
1935
  mergeMultipleTranslationData,
1705
1936
  mergeSheets,
1937
+ normalizeExtension,
1706
1938
  normalizeLocaleCode,
1707
1939
  processRawRows,
1708
1940
  readPublicSheet,
@@ -1712,9 +1944,12 @@ export {
1712
1944
  updateSpreadsheetWithLocalChanges,
1713
1945
  validateCredentials,
1714
1946
  validateEnv,
1947
+ validateImageDirectory,
1715
1948
  wait,
1949
+ walkDirectory,
1716
1950
  withRetry,
1717
1951
  writeLanguageDataFile,
1718
1952
  writeLocalesFile,
1953
+ writeManifest,
1719
1954
  writeTranslationFiles
1720
1955
  };
package/dist/index.d.ts CHANGED
@@ -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":"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,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC7E,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAC1G,YAAY,EACV,oBAAoB,EACpB,+BAA+B,EAC/B,8BAA8B,GAC/B,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,YAAY,EAAE,yBAAyB,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAGxG,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AACzE,YAAY,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAGtH,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,eAAe,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -30,7 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ DEFAULT_IMAGE_EXTENSIONS: () => DEFAULT_IMAGE_EXTENSIONS,
33
34
  DEFAULT_WAIT_SECONDS: () => DEFAULT_WAIT_SECONDS,
35
+ buildManifest: () => buildManifest,
34
36
  convertFromDataJsonFormat: () => convertFromDataJsonFormat,
35
37
  convertToDataJsonFormat: () => convertToDataJsonFormat,
36
38
  createAuthClient: () => createAuthClient,
@@ -52,6 +54,7 @@ __export(index_exports, {
52
54
  manageDriveTranslations: () => manageDriveTranslations,
53
55
  mergeMultipleTranslationData: () => mergeMultipleTranslationData,
54
56
  mergeSheets: () => mergeSheets,
57
+ normalizeExtension: () => normalizeExtension,
55
58
  normalizeLocaleCode: () => normalizeLocaleCode,
56
59
  processRawRows: () => processRawRows,
57
60
  readPublicSheet: () => readPublicSheet,
@@ -61,10 +64,13 @@ __export(index_exports, {
61
64
  updateSpreadsheetWithLocalChanges: () => updateSpreadsheetWithLocalChanges,
62
65
  validateCredentials: () => validateCredentials,
63
66
  validateEnv: () => validateEnv,
67
+ validateImageDirectory: () => validateImageDirectory,
64
68
  wait: () => wait,
69
+ walkDirectory: () => walkDirectory,
65
70
  withRetry: () => withRetry,
66
71
  writeLanguageDataFile: () => writeLanguageDataFile,
67
72
  writeLocalesFile: () => writeLocalesFile,
73
+ writeManifest: () => writeManifest,
68
74
  writeTranslationFiles: () => writeTranslationFiles
69
75
  });
70
76
  module.exports = __toCommonJS(index_exports);
@@ -1537,6 +1543,14 @@ var DEFAULT_IMAGE_MIME_TYPES = [
1537
1543
  ];
1538
1544
  var FOLDER_MIME2 = "application/vnd.google-apps.folder";
1539
1545
  var DRIVE_FILES_URL2 = "https://www.googleapis.com/drive/v3/files";
1546
+ function normalizeExtension(name) {
1547
+ const dot = name.lastIndexOf(".");
1548
+ if (dot === -1) return name;
1549
+ const base = name.slice(0, dot);
1550
+ let ext = name.slice(dot + 1).toLowerCase();
1551
+ if (ext === "jpeg") ext = "jpg";
1552
+ return `${base}.${ext}`;
1553
+ }
1540
1554
  async function getAccessToken2(credentials) {
1541
1555
  const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1542
1556
  const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
@@ -1562,7 +1576,7 @@ async function listFilesInFolder2(folderId, token, mimeTypeFilter) {
1562
1576
  const query = `'${folderId}' in parents${mimeClause} and trashed = false`;
1563
1577
  const params = new URLSearchParams({
1564
1578
  q: query,
1565
- fields: "nextPageToken,files(id,name,mimeType,parents)",
1579
+ fields: "nextPageToken,files(id,name,mimeType,modifiedTime,parents)",
1566
1580
  pageSize: "1000"
1567
1581
  });
1568
1582
  if (pageToken) params.set("pageToken", pageToken);
@@ -1579,7 +1593,7 @@ async function listFilesInFolder2(folderId, token, mimeTypeFilter) {
1579
1593
  } while (pageToken);
1580
1594
  return results;
1581
1595
  }
1582
- async function collectFiles(folderId, folderRelPath, outputPath, token, allowedMimeTypes, recursive, folderPattern) {
1596
+ async function collectFiles(folderId, folderRelPath, outputPath, token, allowedMimeTypes, recursive, folderPattern, normalizeExts = true) {
1583
1597
  console.log(`[driveImageSync] Scanning folder: ${folderId} (path: "${folderRelPath}")`);
1584
1598
  const allItems = await listFilesInFolder2(folderId, token);
1585
1599
  const entries = [];
@@ -1595,12 +1609,20 @@ async function collectFiles(folderId, folderRelPath, outputPath, token, allowedM
1595
1609
  token,
1596
1610
  allowedMimeTypes,
1597
1611
  recursive,
1598
- folderPattern
1612
+ folderPattern,
1613
+ normalizeExts
1599
1614
  );
1600
1615
  entries.push(...subEntries);
1601
1616
  } 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 });
1617
+ const localName = normalizeExts ? normalizeExtension(item.name) : item.name;
1618
+ const localPath = folderRelPath ? (0, import_node_path5.join)(outputPath, folderRelPath, localName) : (0, import_node_path5.join)(outputPath, localName);
1619
+ entries.push({
1620
+ id: item.id,
1621
+ name: item.name,
1622
+ localPath,
1623
+ mimeType: item.mimeType,
1624
+ driveModifiedTime: item.modifiedTime
1625
+ });
1604
1626
  }
1605
1627
  }
1606
1628
  return entries;
@@ -1646,7 +1668,9 @@ async function syncDriveImages(options) {
1646
1668
  folderPattern,
1647
1669
  credentials,
1648
1670
  cleanSync = false,
1649
- concurrency = 3
1671
+ concurrency = 3,
1672
+ incrementalSync = true,
1673
+ normalizeExtensions = true
1650
1674
  } = options;
1651
1675
  const token = await getAccessToken2(credentials);
1652
1676
  (0, import_node_fs6.mkdirSync)(outputPath, { recursive: true });
@@ -1657,16 +1681,33 @@ async function syncDriveImages(options) {
1657
1681
  token,
1658
1682
  mimeTypes,
1659
1683
  recursive,
1660
- folderPattern
1684
+ folderPattern,
1685
+ normalizeExtensions
1661
1686
  );
1662
1687
  const downloaded = [];
1663
1688
  const skipped = [];
1664
1689
  const errors = [];
1665
1690
  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;
1691
+ const localExists = (0, import_node_fs6.existsSync)(entry.localPath);
1692
+ if (localExists) {
1693
+ if (incrementalSync && entry.driveModifiedTime) {
1694
+ try {
1695
+ const localMtimeMs = (0, import_node_fs6.statSync)(entry.localPath).mtimeMs;
1696
+ const driveMtimeMs = new Date(entry.driveModifiedTime).getTime();
1697
+ if (driveMtimeMs <= localMtimeMs) {
1698
+ console.log(`[driveImageSync] Skipping (up to date): ${entry.localPath}`);
1699
+ skipped.push(entry.localPath);
1700
+ return;
1701
+ }
1702
+ console.log(`[driveImageSync] Re-downloading (changed in Drive): ${entry.localPath}`);
1703
+ } catch {
1704
+ console.log(`[driveImageSync] Downloading (could not stat local): ${entry.localPath}`);
1705
+ }
1706
+ } else {
1707
+ console.log(`[driveImageSync] Skipping (exists): ${entry.localPath}`);
1708
+ skipped.push(entry.localPath);
1709
+ return;
1710
+ }
1670
1711
  }
1671
1712
  console.log(`[driveImageSync] Downloading: ${entry.localPath}`);
1672
1713
  try {
@@ -1697,7 +1738,134 @@ async function syncDriveImages(options) {
1697
1738
  return { downloaded, skipped, deleted, errors };
1698
1739
  }
1699
1740
 
1741
+ // src/utils/localImageUtils.ts
1742
+ var import_node_fs7 = require("node:fs");
1743
+ var import_node_path6 = require("node:path");
1744
+ async function walkDirectory(dir, options) {
1745
+ const { extensions } = options ?? {};
1746
+ const lowerExts = extensions?.map((e) => e.toLowerCase());
1747
+ async function go(current) {
1748
+ const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true, encoding: "utf8" });
1749
+ const results = [];
1750
+ for (const entry of entries) {
1751
+ const full = (0, import_node_path6.join)(current, entry.name);
1752
+ if (entry.isDirectory()) {
1753
+ results.push(...await go(full));
1754
+ } else if (entry.isFile()) {
1755
+ if (!lowerExts || lowerExts.includes((0, import_node_path6.extname)(entry.name).toLowerCase())) {
1756
+ results.push(full);
1757
+ }
1758
+ }
1759
+ }
1760
+ return results;
1761
+ }
1762
+ return go(dir);
1763
+ }
1764
+ var DEFAULT_IMAGE_EXTENSIONS = [
1765
+ ".jpg",
1766
+ ".jpeg",
1767
+ ".png",
1768
+ ".webp",
1769
+ ".avif",
1770
+ ".gif",
1771
+ ".svg",
1772
+ ".tiff",
1773
+ ".bmp",
1774
+ ".ico"
1775
+ ];
1776
+ async function validateImageDirectory(options) {
1777
+ const {
1778
+ rootDir,
1779
+ imageExtensions = DEFAULT_IMAGE_EXTENSIONS,
1780
+ allowRootFiles = false,
1781
+ expectedSubfolders = []
1782
+ } = options;
1783
+ const errors = [];
1784
+ const warnings = [];
1785
+ const rootFiles = [];
1786
+ const subfolders = [];
1787
+ const lowerExts = imageExtensions.map((e) => e.toLowerCase());
1788
+ let entries;
1789
+ try {
1790
+ entries = await import_node_fs7.promises.readdir(rootDir, { withFileTypes: true, encoding: "utf8" });
1791
+ } catch (err) {
1792
+ const msg = err instanceof Error ? err.message : String(err);
1793
+ return {
1794
+ valid: false,
1795
+ errors: [`Could not read directory "${rootDir}": ${msg}`],
1796
+ warnings,
1797
+ rootFiles,
1798
+ subfolders
1799
+ };
1800
+ }
1801
+ for (const entry of entries) {
1802
+ if (entry.isDirectory()) {
1803
+ subfolders.push(entry.name);
1804
+ } else if (entry.isFile()) {
1805
+ if (lowerExts.includes((0, import_node_path6.extname)(entry.name).toLowerCase())) {
1806
+ rootFiles.push(entry.name);
1807
+ }
1808
+ }
1809
+ }
1810
+ if (!allowRootFiles && rootFiles.length > 0) {
1811
+ errors.push(
1812
+ `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)` : ""}`
1813
+ );
1814
+ }
1815
+ if (!allowRootFiles && subfolders.length === 0) {
1816
+ errors.push(
1817
+ `No sub-directories found in "${rootDir}". Expected a nested folder structure (e.g. projects/, performances/).`
1818
+ );
1819
+ }
1820
+ const missing = expectedSubfolders.filter((name) => !subfolders.includes(name));
1821
+ if (missing.length > 0) {
1822
+ warnings.push(
1823
+ `Some expected sub-folders are absent from "${rootDir}": ${missing.join(", ")}`
1824
+ );
1825
+ }
1826
+ return {
1827
+ valid: errors.length === 0,
1828
+ errors,
1829
+ warnings,
1830
+ rootFiles,
1831
+ subfolders
1832
+ };
1833
+ }
1834
+
1835
+ // src/utils/getDriveTranslations.ts
1836
+ var import_node_path8 = __toESM(require("node:path"));
1837
+
1838
+ // src/utils/driveProjectIndex.ts
1839
+ var import_node_fs8 = __toESM(require("node:fs"));
1840
+ var import_node_path7 = __toESM(require("node:path"));
1841
+ function buildManifest(options) {
1842
+ const locales = Object.keys(options.translations).sort();
1843
+ return {
1844
+ version: "1",
1845
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1846
+ projectName: options.projectName,
1847
+ domain: options.domain,
1848
+ locales,
1849
+ defaultLocale: options.defaultLocale,
1850
+ spreadsheets: options.spreadsheets,
1851
+ outputDirectory: options.outputDirectory,
1852
+ flatten: options.flatten,
1853
+ projectMetadata: options.projectMetadata
1854
+ };
1855
+ }
1856
+ function writeManifest(manifest, manifestPath) {
1857
+ const dir = import_node_path7.default.dirname(manifestPath);
1858
+ if (!import_node_fs8.default.existsSync(dir)) {
1859
+ import_node_fs8.default.mkdirSync(dir, { recursive: true });
1860
+ }
1861
+ import_node_fs8.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
1862
+ console.log(`[driveProjectIndex] Wrote project manifest \u2192 ${manifestPath}`);
1863
+ }
1864
+
1700
1865
  // src/utils/getDriveTranslations.ts
1866
+ function sanitizeFolderName(name) {
1867
+ return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "sheet";
1868
+ }
1701
1869
  async function manageDriveTranslations(options) {
1702
1870
  const {
1703
1871
  driveFolderId,
@@ -1708,15 +1876,25 @@ async function manageDriveTranslations(options) {
1708
1876
  imageOutputPath,
1709
1877
  imageSyncOptions,
1710
1878
  translationOptions = {},
1711
- docTitles
1879
+ docTitles,
1880
+ flatten = true,
1881
+ createManifest,
1882
+ manifestPath,
1883
+ projectName,
1884
+ domain,
1885
+ defaultLocale,
1886
+ projectMetadata
1712
1887
  } = options;
1713
1888
  if (syncImages && !imageOutputPath) {
1714
1889
  throw new Error(
1715
1890
  "[manageDriveTranslations] imageOutputPath is required when syncImages is true"
1716
1891
  );
1717
1892
  }
1893
+ const shouldCreateManifest = createManifest ?? driveFolderId !== void 0;
1718
1894
  const discoveredIds = [];
1719
1895
  const discoveredNames = /* @__PURE__ */ new Map();
1896
+ const discoveredFolderPaths = /* @__PURE__ */ new Map();
1897
+ const discoveredModifiedTimes = /* @__PURE__ */ new Map();
1720
1898
  if (driveFolderId && scanForSpreadsheets) {
1721
1899
  const scanOptions = { folderId: driveFolderId };
1722
1900
  const discovered = await scanDriveFolderForSpreadsheets(scanOptions);
@@ -1726,6 +1904,8 @@ async function manageDriveTranslations(options) {
1726
1904
  for (const file of discovered) {
1727
1905
  discoveredIds.push(file.id);
1728
1906
  discoveredNames.set(file.id, file.name);
1907
+ discoveredFolderPaths.set(file.id, file.folderPath);
1908
+ if (file.modifiedTime) discoveredModifiedTimes.set(file.id, file.modifiedTime);
1729
1909
  }
1730
1910
  }
1731
1911
  const allIds = [.../* @__PURE__ */ new Set([...discoveredIds, ...explicitIds])];
@@ -1734,10 +1914,50 @@ async function manageDriveTranslations(options) {
1734
1914
  if (!name) return true;
1735
1915
  return spreadsheetNameFilter.test(name);
1736
1916
  }) : allIds;
1737
- const translations = await getMultipleSpreadSheetsData(docTitles, {
1738
- ...translationOptions,
1739
- spreadsheetIds: filteredIds.length > 0 ? filteredIds : void 0
1740
- });
1917
+ let translations;
1918
+ const spreadsheetEntries = [];
1919
+ const baseOutputDir = translationOptions.translationsOutputDir ?? "translations";
1920
+ if (!flatten) {
1921
+ const { mergeStrategy = "later-wins", ...baseOptions } = translationOptions;
1922
+ const perResults = [];
1923
+ for (const id of filteredIds) {
1924
+ const name = discoveredNames.get(id) ?? id;
1925
+ const subDir = sanitizeFolderName(name);
1926
+ const subOutputDir = import_node_path8.default.join(baseOutputDir, subDir);
1927
+ console.log(
1928
+ `[manageDriveTranslations] (flatten: false) Fetching "${name}" \u2192 ${subOutputDir}`
1929
+ );
1930
+ const result = await getSpreadSheetData(docTitles, {
1931
+ ...baseOptions,
1932
+ spreadsheetId: id,
1933
+ translationsOutputDir: subOutputDir
1934
+ });
1935
+ perResults.push(result);
1936
+ spreadsheetEntries.push({
1937
+ id,
1938
+ name,
1939
+ folderPath: discoveredFolderPaths.get(id) ?? "",
1940
+ sheets: docTitles ?? [],
1941
+ modifiedTime: discoveredModifiedTimes.get(id),
1942
+ outputSubDirectory: subDir
1943
+ });
1944
+ }
1945
+ translations = mergeMultipleTranslationData(perResults, mergeStrategy);
1946
+ } else {
1947
+ translations = await getMultipleSpreadSheetsData(docTitles, {
1948
+ ...translationOptions,
1949
+ spreadsheetIds: filteredIds.length > 0 ? filteredIds : void 0
1950
+ });
1951
+ for (const id of filteredIds) {
1952
+ spreadsheetEntries.push({
1953
+ id,
1954
+ name: discoveredNames.get(id) ?? id,
1955
+ folderPath: discoveredFolderPaths.get(id) ?? "",
1956
+ sheets: docTitles ?? [],
1957
+ modifiedTime: discoveredModifiedTimes.get(id)
1958
+ });
1959
+ }
1960
+ }
1741
1961
  let imageSync;
1742
1962
  if (syncImages && driveFolderId && imageOutputPath) {
1743
1963
  imageSync = await syncDriveImages({
@@ -1746,14 +1966,31 @@ async function manageDriveTranslations(options) {
1746
1966
  outputPath: imageOutputPath
1747
1967
  });
1748
1968
  }
1749
- return { translations, spreadsheetIds: filteredIds, imageSync };
1969
+ let manifest;
1970
+ if (shouldCreateManifest) {
1971
+ const resolvedManifestPath = manifestPath ?? import_node_path8.default.join(baseOutputDir, "i18n-manifest.json");
1972
+ manifest = buildManifest({
1973
+ translations,
1974
+ spreadsheets: spreadsheetEntries,
1975
+ outputDirectory: baseOutputDir,
1976
+ flatten,
1977
+ projectName,
1978
+ domain,
1979
+ defaultLocale,
1980
+ projectMetadata
1981
+ });
1982
+ writeManifest(manifest, resolvedManifestPath);
1983
+ }
1984
+ return { translations, spreadsheetIds: filteredIds, imageSync, manifest };
1750
1985
  }
1751
1986
 
1752
1987
  // src/index.ts
1753
1988
  var index_default = getSpreadSheetData;
1754
1989
  // Annotate the CommonJS export names for ESM import in node:
1755
1990
  0 && (module.exports = {
1991
+ DEFAULT_IMAGE_EXTENSIONS,
1756
1992
  DEFAULT_WAIT_SECONDS,
1993
+ buildManifest,
1757
1994
  convertFromDataJsonFormat,
1758
1995
  convertToDataJsonFormat,
1759
1996
  createAuthClient,
@@ -1774,6 +2011,7 @@ var index_default = getSpreadSheetData;
1774
2011
  manageDriveTranslations,
1775
2012
  mergeMultipleTranslationData,
1776
2013
  mergeSheets,
2014
+ normalizeExtension,
1777
2015
  normalizeLocaleCode,
1778
2016
  processRawRows,
1779
2017
  readPublicSheet,
@@ -1783,9 +2021,12 @@ var index_default = getSpreadSheetData;
1783
2021
  updateSpreadsheetWithLocalChanges,
1784
2022
  validateCredentials,
1785
2023
  validateEnv,
2024
+ validateImageDirectory,
1786
2025
  wait,
2026
+ walkDirectory,
1787
2027
  withRetry,
1788
2028
  writeLanguageDataFile,
1789
2029
  writeLocalesFile,
2030
+ writeManifest,
1790
2031
  writeTranslationFiles
1791
2032
  });
@@ -21,6 +21,22 @@ export interface DriveImageSyncOptions {
21
21
  cleanSync?: boolean;
22
22
  /** Max concurrent downloads (default: 3) */
23
23
  concurrency?: number;
24
+ /**
25
+ * When `true` (default), a file that already exists locally is re-downloaded
26
+ * only when Drive's `modifiedTime` is strictly newer than the local file's
27
+ * last-modified timestamp. This avoids re-downloading unchanged assets on
28
+ * every run. When `false`, any file that already exists locally is always
29
+ * skipped regardless of whether Drive has a newer version.
30
+ */
31
+ incrementalSync?: boolean;
32
+ /**
33
+ * When `true` (default), local filenames are written with lowercase file
34
+ * extensions and `jpeg` is normalized to `jpg`.
35
+ * Examples: `Photo.JPEG` → `Photo.jpg`, `banner.PNG` → `banner.png`,
36
+ * `icon.Svg` → `icon.svg`.
37
+ * Applies only to the extension; the base name is left unchanged.
38
+ */
39
+ normalizeExtensions?: boolean;
24
40
  }
25
41
  export interface DriveImageSyncResult {
26
42
  downloaded: string[];
@@ -28,6 +44,14 @@ export interface DriveImageSyncResult {
28
44
  deleted: string[];
29
45
  errors: string[];
30
46
  }
47
+ /**
48
+ * Normalises a filename's extension:
49
+ * - Converts the extension to lowercase
50
+ * - Canonicalises `jpeg` → `jpg`
51
+ *
52
+ * The base name is left unchanged so that `MyPhoto.JPEG` becomes `MyPhoto.jpg`.
53
+ */
54
+ export declare function normalizeExtension(name: string): string;
31
55
  /**
32
56
  * Syncs images from a Google Drive folder to a local directory.
33
57
  * Preserves subfolder structure. Uses Drive API v3 via fetch.
@@ -1 +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"}
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;IACrB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;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;AAwCD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOvD;AAyJD;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CA4F/B"}
@@ -0,0 +1,74 @@
1
+ import type { TranslationData } from '../types';
2
+ /**
3
+ * Metadata for a single spreadsheet in the project manifest.
4
+ */
5
+ export interface SpreadsheetManifestEntry {
6
+ /** Google Spreadsheet file ID */
7
+ id: string;
8
+ /** Human-readable name of the spreadsheet */
9
+ name: string;
10
+ /** Relative path within the Drive folder (e.g. "subproject/translations") */
11
+ folderPath: string;
12
+ /** Sheet / tab names that were processed */
13
+ sheets: string[];
14
+ /** ISO timestamp of last modification reported by Drive */
15
+ modifiedTime?: string;
16
+ /**
17
+ * Local subdirectory used for output when `flatten: false`.
18
+ * Undefined when `flatten: true` (all locales go to the root outputDirectory).
19
+ */
20
+ outputSubDirectory?: string;
21
+ }
22
+ /**
23
+ * Project-level manifest written to disk after every `manageDriveTranslations` run.
24
+ * Acts as a single source of truth for the i18n project layout.
25
+ */
26
+ export interface DriveProjectManifest {
27
+ /** Manifest format version — increment when the shape changes */
28
+ version: '1';
29
+ /** ISO timestamp when this manifest was last generated */
30
+ generatedAt: string;
31
+ /** User-defined project name (e.g. "my-app-i18n") */
32
+ projectName?: string;
33
+ /** Project domain or site URL for reference */
34
+ domain?: string;
35
+ /** Sorted list of all locale codes available across all spreadsheets */
36
+ locales: string[];
37
+ /** Primary / source locale (e.g. "en") */
38
+ defaultLocale?: string;
39
+ /** Every spreadsheet that was processed in the last run */
40
+ spreadsheets: SpreadsheetManifestEntry[];
41
+ /** Base local directory where translation files are written */
42
+ outputDirectory: string;
43
+ /**
44
+ * Whether translation files use a flat layout (all locales in one dir)
45
+ * or a per-spreadsheet subdirectory layout.
46
+ */
47
+ flatten: boolean;
48
+ /** Any additional user-defined metadata */
49
+ projectMetadata?: Record<string, unknown>;
50
+ }
51
+ export interface BuildManifestOptions {
52
+ translations: TranslationData;
53
+ spreadsheets: SpreadsheetManifestEntry[];
54
+ outputDirectory: string;
55
+ flatten: boolean;
56
+ projectName?: string;
57
+ domain?: string;
58
+ defaultLocale?: string;
59
+ projectMetadata?: Record<string, unknown>;
60
+ }
61
+ /**
62
+ * Builds a DriveProjectManifest from the current run's state.
63
+ * Does NOT write to disk — call `writeManifest` for that.
64
+ */
65
+ export declare function buildManifest(options: BuildManifestOptions): DriveProjectManifest;
66
+ /**
67
+ * Writes the project manifest JSON to disk.
68
+ * Creates parent directories as needed.
69
+ *
70
+ * @param manifest - The manifest to serialize
71
+ * @param manifestPath - Absolute or relative path for the output file
72
+ */
73
+ export declare function writeManifest(manifest: DriveProjectManifest, manifestPath: string): void;
74
+ //# sourceMappingURL=driveProjectIndex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driveProjectIndex.d.ts","sourceRoot":"","sources":["../../src/utils/driveProjectIndex.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEhD;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,iCAAiC;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,OAAO,EAAE,GAAG,CAAC;IACb,0DAA0D;IAC1D,WAAW,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,0CAA0C;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,YAAY,EAAE,wBAAwB,EAAE,CAAC;IACzC,+DAA+D;IAC/D,eAAe,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB,2CAA2C;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,EAAE,eAAe,CAAC;IAC9B,YAAY,EAAE,wBAAwB,EAAE,CAAC;IACzC,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,oBAAoB,CAcjF;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAOxF"}
@@ -1,6 +1,7 @@
1
1
  import type { TranslationData } from '../types';
2
2
  import type { MultiSpreadsheetOptions } from '../getMultipleSpreadSheetsData';
3
3
  import type { DriveImageSyncOptions, DriveImageSyncResult } from './driveImageSync';
4
+ import type { DriveProjectManifest } from './driveProjectIndex';
4
5
  export interface GoogleDriveManagerOptions {
5
6
  /**
6
7
  * Google Drive folder ID to scan for spreadsheets and/or images.
@@ -47,6 +48,34 @@ export interface GoogleDriveManagerOptions {
47
48
  translationOptions?: MultiSpreadsheetOptions;
48
49
  /** Sheet names to fetch from each discovered spreadsheet */
49
50
  docTitles?: string[];
51
+ /**
52
+ * When `false`, each spreadsheet writes translations to its own subdirectory
53
+ * inside `translationsOutputDir`, named after the spreadsheet (sanitized).
54
+ * Example with `flatten: false`:
55
+ * `translations/app-i18n/en.json`
56
+ * `translations/marketing/de.json`
57
+ * When `true` (default), all spreadsheets are merged into a flat set:
58
+ * `translations/en.json`
59
+ */
60
+ flatten?: boolean;
61
+ /**
62
+ * Write an `i18n-manifest.json` index file after each run.
63
+ * Default: `true` when `driveFolderId` is set, `false` otherwise.
64
+ */
65
+ createManifest?: boolean;
66
+ /**
67
+ * Path for the manifest file.
68
+ * Default: `path.join(translationsOutputDir, 'i18n-manifest.json')`
69
+ */
70
+ manifestPath?: string;
71
+ /** Human-readable project name stored in the manifest */
72
+ projectName?: string;
73
+ /** Site domain / URL stored in the manifest */
74
+ domain?: string;
75
+ /** Primary locale code stored in the manifest (e.g. "en") */
76
+ defaultLocale?: string;
77
+ /** Arbitrary metadata stored in the manifest */
78
+ projectMetadata?: Record<string, unknown>;
50
79
  }
51
80
  export interface GoogleDriveManagerResult {
52
81
  translations: TranslationData;
@@ -54,6 +83,8 @@ export interface GoogleDriveManagerResult {
54
83
  spreadsheetIds: string[];
55
84
  /** Image sync result (only present if syncImages: true) */
56
85
  imageSync?: DriveImageSyncResult;
86
+ /** Project manifest written during this run (only present when `createManifest: true`) */
87
+ manifest?: DriveProjectManifest;
57
88
  }
58
89
  /**
59
90
  * Top-level "headless CMS bridge" function.
@@ -1 +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"}
1
+ {"version":3,"file":"getDriveTranslations.d.ts","sourceRoot":"","sources":["../../src/utils/getDriveTranslations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAM9E,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAEpF,OAAO,KAAK,EAAE,oBAAoB,EAA4B,MAAM,qBAAqB,CAAC;AAG1F,MAAM,WAAW,yBAAyB;IACxC;;;;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;IAErB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,eAAe,CAAC;IAC9B,kDAAkD;IAClD,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,oBAAoB,CAAC;IACjC,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CACjC;AAaD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CAiJnC"}
@@ -0,0 +1,105 @@
1
+ export interface WalkDirectoryOptions {
2
+ /**
3
+ * Only include files whose extension (case-insensitive, including the dot)
4
+ * is in this list. Example: `['.jpg', '.png']`.
5
+ * When omitted, all files are included.
6
+ */
7
+ extensions?: string[];
8
+ }
9
+ /**
10
+ * Recursively walks a directory and returns the absolute paths of all files
11
+ * found inside it (and any sub-directories).
12
+ *
13
+ * Uses `fs/promises` throughout so it integrates cleanly with async pipelines.
14
+ *
15
+ * @param dir - Absolute or relative path to the directory to walk.
16
+ * @param options - Optional filter options.
17
+ * @returns Array of absolute file paths.
18
+ *
19
+ * @example
20
+ * const files = await walkDirectory('./src/assets/remote-images', {
21
+ * extensions: ['.jpg', '.png'],
22
+ * });
23
+ * console.log(files); // ['/abs/path/projects/hero.jpg', ...]
24
+ */
25
+ export declare function walkDirectory(dir: string, options?: WalkDirectoryOptions): Promise<string[]>;
26
+ /** Default extensions treated as "image files" for validation purposes. */
27
+ export declare const DEFAULT_IMAGE_EXTENSIONS: string[];
28
+ export interface ImageDirectoryValidationOptions {
29
+ /**
30
+ * Absolute or relative path to the root image directory to inspect
31
+ * (e.g. `'./src/assets/remote-images'`).
32
+ */
33
+ rootDir: string;
34
+ /**
35
+ * File extensions (lower-case, with leading dot) that are counted as
36
+ * image files.
37
+ * Defaults to `DEFAULT_IMAGE_EXTENSIONS`.
38
+ */
39
+ imageExtensions?: string[];
40
+ /**
41
+ * When `false` (default), the presence of image files directly inside
42
+ * `rootDir` (rather than inside sub-directories) is treated as an **error**
43
+ * — it typically means the sync inadvertently flattened the folder
44
+ * hierarchy.
45
+ * Set to `true` to allow root-level images without raising an error.
46
+ */
47
+ allowRootFiles?: boolean;
48
+ /**
49
+ * Sub-folder names that are expected to be present inside `rootDir`.
50
+ * Any names from this list that are absent produce a **warning** (not an
51
+ * error), because the folder may simply be empty in Drive.
52
+ *
53
+ * Example: `['projects', 'performances', 'workshops']`
54
+ */
55
+ expectedSubfolders?: string[];
56
+ }
57
+ export interface ImageDirectoryValidationResult {
58
+ /**
59
+ * `true` when no errors were found.
60
+ * Warnings do **not** affect this flag.
61
+ */
62
+ valid: boolean;
63
+ /** Fatal problems that indicate incorrect state. */
64
+ errors: string[];
65
+ /** Non-fatal observations that may indicate a problem. */
66
+ warnings: string[];
67
+ /**
68
+ * Names of image files found directly in `rootDir` (not in sub-directories).
69
+ * Populated even when `allowRootFiles` is `true`.
70
+ */
71
+ rootFiles: string[];
72
+ /** Names of all direct sub-directories found in `rootDir`. */
73
+ subfolders: string[];
74
+ }
75
+ /**
76
+ * Inspects a local image directory and validates that it has the expected
77
+ * nested structure after a `syncDriveImages` call.
78
+ *
79
+ * Three things are checked:
80
+ *
81
+ * 1. **Root-level image files** — by default these are an error because they
82
+ * usually indicate the sync flattened the Drive folder hierarchy.
83
+ * 2. **Presence of sub-directories** — at least one sub-directory must exist
84
+ * (unless `allowRootFiles: true`).
85
+ * 3. **Expected sub-folder names** — if `expectedSubfolders` is provided, any
86
+ * missing names produce a warning.
87
+ *
88
+ * The function never throws; all problems are reported in the returned object.
89
+ *
90
+ * @example
91
+ * import { validateImageDirectory } from '@el-j/google-sheet-translations';
92
+ *
93
+ * const result = await validateImageDirectory({
94
+ * rootDir: './src/assets/remote-images',
95
+ * expectedSubfolders: ['projects', 'performances', 'workshops'],
96
+ * });
97
+ *
98
+ * if (!result.valid) {
99
+ * console.error('Image directory problems:', result.errors);
100
+ * process.exit(1);
101
+ * }
102
+ * for (const warn of result.warnings) console.warn(warn);
103
+ */
104
+ export declare function validateImageDirectory(options: ImageDirectoryValidationOptions): Promise<ImageDirectoryValidationResult>;
105
+ //# sourceMappingURL=localImageUtils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localImageUtils.d.ts","sourceRoot":"","sources":["../../src/utils/localImageUtils.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CAuBnB;AAMD,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB,UAWpC,CAAC;AAEF,MAAM,WAAW,+BAA+B;IAC9C;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAE3B;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,8BAA8B;IAC7C;;;OAGG;IACH,KAAK,EAAE,OAAO,CAAC;IACf,oDAAoD;IACpD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;;;OAGG;IACH,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,8BAA8B,CAAC,CAsEzC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@el-j/google-sheet-translations",
3
- "version": "2.2.0-beta.1",
3
+ "version": "2.2.0-beta.2",
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",
@@ -39,7 +39,8 @@
39
39
  "docs:build": "vitepress build website",
40
40
  "docs:preview": "vitepress preview website",
41
41
  "test:integration": "INTEGRATION=true jest --testPathPatterns=integration --testTimeout=60000 --coverage=false",
42
- "sync:translations": "node scripts/sync-demo-translations.mjs"
42
+ "sync:translations": "node scripts/sync-demo-translations.mjs",
43
+ "changelog:preview": "node scripts/changelog-preview.mjs"
43
44
  },
44
45
  "keywords": [
45
46
  "google-sheets",