@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 +6 -0
- package/dist/esm/index.js +252 -17
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +258 -17
- package/dist/utils/driveImageSync.d.ts +24 -0
- package/dist/utils/driveImageSync.d.ts.map +1 -1
- package/dist/utils/driveProjectIndex.d.ts +74 -0
- package/dist/utils/driveProjectIndex.d.ts.map +1 -0
- package/dist/utils/getDriveTranslations.d.ts +31 -0
- package/dist/utils/getDriveTranslations.d.ts.map +1 -1
- package/dist/utils/localImageUtils.d.ts +105 -0
- package/dist/utils/localImageUtils.d.ts.map +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Google Sheet Translations
|
|
2
2
|
|
|
3
|
+
[](https://github.com/el-j/google-sheet-translations/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/el-j/google-sheet-translations/actions/workflows/release.yml)
|
|
5
|
+
[](https://github.com/el-j/google-sheet-translations/actions/workflows/docs.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/@el-j/google-sheet-translations)
|
|
7
|
+
[](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
|
|
1532
|
-
|
|
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
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
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
|
-
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
1603
|
-
|
|
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
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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
|
-
|
|
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;
|
|
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":"
|
|
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.
|
|
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",
|