@el-j/google-sheet-translations 2.1.5-beta.1 → 2.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/action-entrypoint.d.ts.map +1 -1
- package/dist/esm/index.js +601 -0
- package/dist/getMultipleSpreadSheetsData.d.ts +19 -0
- package/dist/getMultipleSpreadSheetsData.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +612 -0
- package/dist/utils/driveFolderScanner.d.ts +26 -0
- package/dist/utils/driveFolderScanner.d.ts.map +1 -0
- package/dist/utils/driveImageSync.d.ts +68 -0
- package/dist/utils/driveImageSync.d.ts.map +1 -0
- package/dist/utils/driveProjectIndex.d.ts +74 -0
- package/dist/utils/driveProjectIndex.d.ts.map +1 -0
- package/dist/utils/getDriveTranslations.d.ts +111 -0
- package/dist/utils/getDriveTranslations.d.ts.map +1 -0
- package/dist/utils/localImageUtils.d.ts +105 -0
- package/dist/utils/localImageUtils.d.ts.map +1 -0
- package/dist/utils/multiSpreadsheetMerger.d.ts +7 -0
- package/dist/utils/multiSpreadsheetMerger.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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-entrypoint.d.ts","sourceRoot":"","sources":["../src/action-entrypoint.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"action-entrypoint.d.ts","sourceRoot":"","sources":["../src/action-entrypoint.ts"],"names":[],"mappings":"AAMA,wBAAsB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CA2FzC"}
|
package/dist/esm/index.js
CHANGED
|
@@ -1317,10 +1317,602 @@ function mergeSheets(translations, locale, sheetNames) {
|
|
|
1317
1317
|
return merged;
|
|
1318
1318
|
}
|
|
1319
1319
|
|
|
1320
|
+
// src/utils/multiSpreadsheetMerger.ts
|
|
1321
|
+
function mergeMultipleTranslationData(results, mergeStrategy = "later-wins") {
|
|
1322
|
+
const merged = {};
|
|
1323
|
+
for (const result of results) {
|
|
1324
|
+
for (const [locale, sheets] of Object.entries(result)) {
|
|
1325
|
+
if (!merged[locale]) {
|
|
1326
|
+
merged[locale] = {};
|
|
1327
|
+
}
|
|
1328
|
+
for (const [sheet, keys] of Object.entries(sheets)) {
|
|
1329
|
+
if (!merged[locale][sheet]) {
|
|
1330
|
+
merged[locale][sheet] = {};
|
|
1331
|
+
}
|
|
1332
|
+
for (const [key, value] of Object.entries(keys)) {
|
|
1333
|
+
if (mergeStrategy === "first-wins" && key in merged[locale][sheet]) {
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
merged[locale][sheet][key] = value;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return merged;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/getMultipleSpreadSheetsData.ts
|
|
1345
|
+
async function getMultipleSpreadSheetsData(docTitles, options = {}) {
|
|
1346
|
+
const { spreadsheetIds, mergeStrategy = "later-wins", ...baseOptions } = options;
|
|
1347
|
+
if (!spreadsheetIds || spreadsheetIds.length === 0) {
|
|
1348
|
+
return getSpreadSheetData(docTitles, baseOptions);
|
|
1349
|
+
}
|
|
1350
|
+
console.log(`[getMultipleSpreadSheetsData] Fetching ${spreadsheetIds.length} spreadsheets...`);
|
|
1351
|
+
const results = [];
|
|
1352
|
+
for (let i = 0; i < spreadsheetIds.length; i++) {
|
|
1353
|
+
const id = spreadsheetIds[i];
|
|
1354
|
+
console.log(`[getMultipleSpreadSheetsData] (${i + 1}/${spreadsheetIds.length}) "${id}"...`);
|
|
1355
|
+
const result = await getSpreadSheetData(docTitles, { ...baseOptions, spreadsheetId: id });
|
|
1356
|
+
results.push(result);
|
|
1357
|
+
}
|
|
1358
|
+
return mergeMultipleTranslationData(results, mergeStrategy);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/utils/driveFolderScanner.ts
|
|
1362
|
+
import { GoogleAuth } from "google-auth-library";
|
|
1363
|
+
var SPREADSHEET_MIME = "application/vnd.google-apps.spreadsheet";
|
|
1364
|
+
var FOLDER_MIME = "application/vnd.google-apps.folder";
|
|
1365
|
+
var DRIVE_FILES_URL = "https://www.googleapis.com/drive/v3/files";
|
|
1366
|
+
async function getAccessToken(credentials) {
|
|
1367
|
+
const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
|
|
1368
|
+
const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
|
|
1369
|
+
if (!clientEmail || !privateKey) {
|
|
1370
|
+
throw new Error(
|
|
1371
|
+
"Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
const normalizedKey = privateKey.replace(/\\n/g, "\n");
|
|
1375
|
+
const auth = new GoogleAuth({
|
|
1376
|
+
credentials: { client_email: clientEmail, private_key: normalizedKey },
|
|
1377
|
+
scopes: ["https://www.googleapis.com/auth/drive.readonly"]
|
|
1378
|
+
});
|
|
1379
|
+
const client = await auth.getClient();
|
|
1380
|
+
const tokenResponse = await client.getAccessToken();
|
|
1381
|
+
return tokenResponse.token;
|
|
1382
|
+
}
|
|
1383
|
+
async function listFilesInFolder(folderId, mimeType, token) {
|
|
1384
|
+
const results = [];
|
|
1385
|
+
let pageToken;
|
|
1386
|
+
do {
|
|
1387
|
+
const query = `'${folderId}' in parents and mimeType = '${mimeType}' and trashed = false`;
|
|
1388
|
+
const params = new URLSearchParams({
|
|
1389
|
+
q: query,
|
|
1390
|
+
fields: "nextPageToken,files(id,name,mimeType,modifiedTime,parents)",
|
|
1391
|
+
pageSize: "1000"
|
|
1392
|
+
});
|
|
1393
|
+
if (pageToken) params.set("pageToken", pageToken);
|
|
1394
|
+
const response = await fetch(`${DRIVE_FILES_URL}?${params.toString()}`, {
|
|
1395
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1396
|
+
});
|
|
1397
|
+
if (!response.ok) {
|
|
1398
|
+
const text = await response.text();
|
|
1399
|
+
throw new Error(
|
|
1400
|
+
`Drive API error ${response.status}: ${text}`
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
const data = await response.json();
|
|
1404
|
+
results.push(...data.files);
|
|
1405
|
+
pageToken = data.nextPageToken;
|
|
1406
|
+
} while (pageToken);
|
|
1407
|
+
return results;
|
|
1408
|
+
}
|
|
1409
|
+
async function scanFolder(folderId, folderPath, token, recursive, nameFilter, seen = /* @__PURE__ */ new Set()) {
|
|
1410
|
+
console.log(`[driveFolderScanner] Scanning folder: ${folderId} (path: "${folderPath}")`);
|
|
1411
|
+
const spreadsheets = await listFilesInFolder(folderId, SPREADSHEET_MIME, token);
|
|
1412
|
+
const results = [];
|
|
1413
|
+
for (const file of spreadsheets) {
|
|
1414
|
+
if (seen.has(file.id)) continue;
|
|
1415
|
+
seen.add(file.id);
|
|
1416
|
+
if (nameFilter && !nameFilter.test(file.name)) continue;
|
|
1417
|
+
results.push({
|
|
1418
|
+
id: file.id,
|
|
1419
|
+
name: file.name,
|
|
1420
|
+
folderPath,
|
|
1421
|
+
mimeType: file.mimeType,
|
|
1422
|
+
modifiedTime: file.modifiedTime
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
if (recursive) {
|
|
1426
|
+
const subfolders = await listFilesInFolder(folderId, FOLDER_MIME, token);
|
|
1427
|
+
for (const folder of subfolders) {
|
|
1428
|
+
const subPath = folderPath ? `${folderPath}/${folder.name}` : folder.name;
|
|
1429
|
+
const subResults = await scanFolder(
|
|
1430
|
+
folder.id,
|
|
1431
|
+
subPath,
|
|
1432
|
+
token,
|
|
1433
|
+
recursive,
|
|
1434
|
+
nameFilter,
|
|
1435
|
+
seen
|
|
1436
|
+
);
|
|
1437
|
+
results.push(...subResults);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return results;
|
|
1441
|
+
}
|
|
1442
|
+
async function scanDriveFolderForSpreadsheets(options) {
|
|
1443
|
+
const { folderId, recursive = true, nameFilter, credentials } = options;
|
|
1444
|
+
const token = await getAccessToken(credentials);
|
|
1445
|
+
return scanFolder(folderId, "", token, recursive, nameFilter);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// src/utils/driveImageSync.ts
|
|
1449
|
+
import { createWriteStream, mkdirSync, existsSync, readdirSync, unlinkSync, statSync } from "node:fs";
|
|
1450
|
+
import { join, dirname } from "node:path";
|
|
1451
|
+
import { pipeline } from "node:stream/promises";
|
|
1452
|
+
import { Readable } from "node:stream";
|
|
1453
|
+
import { GoogleAuth as GoogleAuth2 } from "google-auth-library";
|
|
1454
|
+
var DEFAULT_IMAGE_MIME_TYPES = [
|
|
1455
|
+
"image/jpeg",
|
|
1456
|
+
"image/jpg",
|
|
1457
|
+
"image/png",
|
|
1458
|
+
"image/webp",
|
|
1459
|
+
"image/avif",
|
|
1460
|
+
"image/gif",
|
|
1461
|
+
"image/svg+xml",
|
|
1462
|
+
"image/tiff",
|
|
1463
|
+
"image/bmp",
|
|
1464
|
+
"image/ico",
|
|
1465
|
+
"image/x-icon"
|
|
1466
|
+
];
|
|
1467
|
+
var FOLDER_MIME2 = "application/vnd.google-apps.folder";
|
|
1468
|
+
var DRIVE_FILES_URL2 = "https://www.googleapis.com/drive/v3/files";
|
|
1469
|
+
function normalizeExtension(name) {
|
|
1470
|
+
const dot = name.lastIndexOf(".");
|
|
1471
|
+
if (dot === -1) return name;
|
|
1472
|
+
const base = name.slice(0, dot);
|
|
1473
|
+
let ext = name.slice(dot + 1).toLowerCase();
|
|
1474
|
+
if (ext === "jpeg") ext = "jpg";
|
|
1475
|
+
return `${base}.${ext}`;
|
|
1476
|
+
}
|
|
1477
|
+
async function getAccessToken2(credentials) {
|
|
1478
|
+
const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
|
|
1479
|
+
const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
|
|
1480
|
+
if (!clientEmail || !privateKey) {
|
|
1481
|
+
throw new Error(
|
|
1482
|
+
"Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
const normalizedKey = privateKey.replace(/\\n/g, "\n");
|
|
1486
|
+
const auth = new GoogleAuth2({
|
|
1487
|
+
credentials: { client_email: clientEmail, private_key: normalizedKey },
|
|
1488
|
+
scopes: ["https://www.googleapis.com/auth/drive.readonly"]
|
|
1489
|
+
});
|
|
1490
|
+
const client = await auth.getClient();
|
|
1491
|
+
const tokenResponse = await client.getAccessToken();
|
|
1492
|
+
return tokenResponse.token;
|
|
1493
|
+
}
|
|
1494
|
+
async function listFilesInFolder2(folderId, token, mimeTypeFilter) {
|
|
1495
|
+
const results = [];
|
|
1496
|
+
let pageToken;
|
|
1497
|
+
do {
|
|
1498
|
+
const mimeClause = mimeTypeFilter ? ` and mimeType = '${mimeTypeFilter}'` : "";
|
|
1499
|
+
const query = `'${folderId}' in parents${mimeClause} and trashed = false`;
|
|
1500
|
+
const params = new URLSearchParams({
|
|
1501
|
+
q: query,
|
|
1502
|
+
fields: "nextPageToken,files(id,name,mimeType,modifiedTime,parents)",
|
|
1503
|
+
pageSize: "1000"
|
|
1504
|
+
});
|
|
1505
|
+
if (pageToken) params.set("pageToken", pageToken);
|
|
1506
|
+
const response = await fetch(`${DRIVE_FILES_URL2}?${params.toString()}`, {
|
|
1507
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1508
|
+
});
|
|
1509
|
+
if (!response.ok) {
|
|
1510
|
+
const text = await response.text();
|
|
1511
|
+
throw new Error(`Drive API error ${response.status}: ${text}`);
|
|
1512
|
+
}
|
|
1513
|
+
const data = await response.json();
|
|
1514
|
+
results.push(...data.files);
|
|
1515
|
+
pageToken = data.nextPageToken;
|
|
1516
|
+
} while (pageToken);
|
|
1517
|
+
return results;
|
|
1518
|
+
}
|
|
1519
|
+
async function collectFiles(folderId, folderRelPath, outputPath, token, allowedMimeTypes, recursive, folderPattern, normalizeExts = true) {
|
|
1520
|
+
console.log(`[driveImageSync] Scanning folder: ${folderId} (path: "${folderRelPath}")`);
|
|
1521
|
+
const allItems = await listFilesInFolder2(folderId, token);
|
|
1522
|
+
const entries = [];
|
|
1523
|
+
for (const item of allItems) {
|
|
1524
|
+
if (item.mimeType === FOLDER_MIME2) {
|
|
1525
|
+
if (!recursive) continue;
|
|
1526
|
+
const subRelPath = folderRelPath ? `${folderRelPath}/${item.name}` : item.name;
|
|
1527
|
+
if (folderPattern && !folderPattern.test(subRelPath)) continue;
|
|
1528
|
+
const subEntries = await collectFiles(
|
|
1529
|
+
item.id,
|
|
1530
|
+
subRelPath,
|
|
1531
|
+
outputPath,
|
|
1532
|
+
token,
|
|
1533
|
+
allowedMimeTypes,
|
|
1534
|
+
recursive,
|
|
1535
|
+
folderPattern,
|
|
1536
|
+
normalizeExts
|
|
1537
|
+
);
|
|
1538
|
+
entries.push(...subEntries);
|
|
1539
|
+
} else if (allowedMimeTypes.includes(item.mimeType)) {
|
|
1540
|
+
const localName = normalizeExts ? normalizeExtension(item.name) : item.name;
|
|
1541
|
+
const localPath = folderRelPath ? join(outputPath, folderRelPath, localName) : join(outputPath, localName);
|
|
1542
|
+
entries.push({
|
|
1543
|
+
id: item.id,
|
|
1544
|
+
name: item.name,
|
|
1545
|
+
localPath,
|
|
1546
|
+
mimeType: item.mimeType,
|
|
1547
|
+
driveModifiedTime: item.modifiedTime
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return entries;
|
|
1552
|
+
}
|
|
1553
|
+
async function downloadFile(fileId, localPath, token) {
|
|
1554
|
+
const url = `${DRIVE_FILES_URL2}/${fileId}?alt=media`;
|
|
1555
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
1556
|
+
if (!response.ok) {
|
|
1557
|
+
throw new Error(`Failed to download ${fileId}: ${response.status}`);
|
|
1558
|
+
}
|
|
1559
|
+
mkdirSync(dirname(localPath), { recursive: true });
|
|
1560
|
+
const dest = createWriteStream(localPath);
|
|
1561
|
+
await pipeline(Readable.fromWeb(response.body), dest);
|
|
1562
|
+
}
|
|
1563
|
+
function collectLocalFiles(dir, base) {
|
|
1564
|
+
const results = [];
|
|
1565
|
+
if (!existsSync(dir)) return results;
|
|
1566
|
+
for (const entry of readdirSync(dir)) {
|
|
1567
|
+
const fullPath = join(dir, entry);
|
|
1568
|
+
const stat = statSync(fullPath);
|
|
1569
|
+
if (stat.isDirectory()) {
|
|
1570
|
+
results.push(...collectLocalFiles(fullPath, base));
|
|
1571
|
+
} else {
|
|
1572
|
+
results.push(fullPath);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return results;
|
|
1576
|
+
}
|
|
1577
|
+
async function runConcurrent(tasks, concurrency) {
|
|
1578
|
+
const results = [];
|
|
1579
|
+
for (let i = 0; i < tasks.length; i += concurrency) {
|
|
1580
|
+
const batch = tasks.slice(i, i + concurrency).map((t) => t());
|
|
1581
|
+
results.push(...await Promise.all(batch));
|
|
1582
|
+
}
|
|
1583
|
+
return results;
|
|
1584
|
+
}
|
|
1585
|
+
async function syncDriveImages(options) {
|
|
1586
|
+
const {
|
|
1587
|
+
folderId,
|
|
1588
|
+
outputPath,
|
|
1589
|
+
mimeTypes = DEFAULT_IMAGE_MIME_TYPES,
|
|
1590
|
+
recursive = true,
|
|
1591
|
+
folderPattern,
|
|
1592
|
+
credentials,
|
|
1593
|
+
cleanSync = false,
|
|
1594
|
+
concurrency = 3,
|
|
1595
|
+
incrementalSync = true,
|
|
1596
|
+
normalizeExtensions = true
|
|
1597
|
+
} = options;
|
|
1598
|
+
const token = await getAccessToken2(credentials);
|
|
1599
|
+
mkdirSync(outputPath, { recursive: true });
|
|
1600
|
+
const entries = await collectFiles(
|
|
1601
|
+
folderId,
|
|
1602
|
+
"",
|
|
1603
|
+
outputPath,
|
|
1604
|
+
token,
|
|
1605
|
+
mimeTypes,
|
|
1606
|
+
recursive,
|
|
1607
|
+
folderPattern,
|
|
1608
|
+
normalizeExtensions
|
|
1609
|
+
);
|
|
1610
|
+
const downloaded = [];
|
|
1611
|
+
const skipped = [];
|
|
1612
|
+
const errors = [];
|
|
1613
|
+
const tasks = entries.map((entry) => async () => {
|
|
1614
|
+
const localExists = existsSync(entry.localPath);
|
|
1615
|
+
if (localExists) {
|
|
1616
|
+
if (incrementalSync && entry.driveModifiedTime) {
|
|
1617
|
+
try {
|
|
1618
|
+
const localMtimeMs = statSync(entry.localPath).mtimeMs;
|
|
1619
|
+
const driveMtimeMs = new Date(entry.driveModifiedTime).getTime();
|
|
1620
|
+
if (driveMtimeMs <= localMtimeMs) {
|
|
1621
|
+
console.log(`[driveImageSync] Skipping (up to date): ${entry.localPath}`);
|
|
1622
|
+
skipped.push(entry.localPath);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
console.log(`[driveImageSync] Re-downloading (changed in Drive): ${entry.localPath}`);
|
|
1626
|
+
} catch {
|
|
1627
|
+
console.log(`[driveImageSync] Downloading (could not stat local): ${entry.localPath}`);
|
|
1628
|
+
}
|
|
1629
|
+
} else {
|
|
1630
|
+
console.log(`[driveImageSync] Skipping (exists): ${entry.localPath}`);
|
|
1631
|
+
skipped.push(entry.localPath);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
console.log(`[driveImageSync] Downloading: ${entry.localPath}`);
|
|
1636
|
+
try {
|
|
1637
|
+
await downloadFile(entry.id, entry.localPath, token);
|
|
1638
|
+
downloaded.push(entry.localPath);
|
|
1639
|
+
} catch (err) {
|
|
1640
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1641
|
+
console.error(`[driveImageSync] Error downloading ${entry.localPath}: ${msg}`);
|
|
1642
|
+
errors.push(entry.localPath);
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
await runConcurrent(tasks, concurrency);
|
|
1646
|
+
const deleted = [];
|
|
1647
|
+
if (cleanSync) {
|
|
1648
|
+
const driveLocalPaths = new Set(entries.map((e) => e.localPath));
|
|
1649
|
+
const localFiles = collectLocalFiles(outputPath, outputPath);
|
|
1650
|
+
for (const localFile of localFiles) {
|
|
1651
|
+
if (!driveLocalPaths.has(localFile)) {
|
|
1652
|
+
console.log(`[driveImageSync] Deleting (not in Drive): ${localFile}`);
|
|
1653
|
+
unlinkSync(localFile);
|
|
1654
|
+
deleted.push(localFile);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
console.log(
|
|
1659
|
+
`[driveImageSync] Synced ${downloaded.length} files, skipped ${skipped.length}, deleted ${deleted.length}, errors ${errors.length}`
|
|
1660
|
+
);
|
|
1661
|
+
return { downloaded, skipped, deleted, errors };
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// src/utils/localImageUtils.ts
|
|
1665
|
+
import { promises as fsp } from "node:fs";
|
|
1666
|
+
import { join as join2, extname } from "node:path";
|
|
1667
|
+
async function walkDirectory(dir, options) {
|
|
1668
|
+
const { extensions } = options ?? {};
|
|
1669
|
+
const lowerExts = extensions?.map((e) => e.toLowerCase());
|
|
1670
|
+
async function go(current) {
|
|
1671
|
+
const entries = await fsp.readdir(current, { withFileTypes: true, encoding: "utf8" });
|
|
1672
|
+
const results = [];
|
|
1673
|
+
for (const entry of entries) {
|
|
1674
|
+
const full = join2(current, entry.name);
|
|
1675
|
+
if (entry.isDirectory()) {
|
|
1676
|
+
results.push(...await go(full));
|
|
1677
|
+
} else if (entry.isFile()) {
|
|
1678
|
+
if (!lowerExts || lowerExts.includes(extname(entry.name).toLowerCase())) {
|
|
1679
|
+
results.push(full);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return results;
|
|
1684
|
+
}
|
|
1685
|
+
return go(dir);
|
|
1686
|
+
}
|
|
1687
|
+
var DEFAULT_IMAGE_EXTENSIONS = [
|
|
1688
|
+
".jpg",
|
|
1689
|
+
".jpeg",
|
|
1690
|
+
".png",
|
|
1691
|
+
".webp",
|
|
1692
|
+
".avif",
|
|
1693
|
+
".gif",
|
|
1694
|
+
".svg",
|
|
1695
|
+
".tiff",
|
|
1696
|
+
".bmp",
|
|
1697
|
+
".ico"
|
|
1698
|
+
];
|
|
1699
|
+
async function validateImageDirectory(options) {
|
|
1700
|
+
const {
|
|
1701
|
+
rootDir,
|
|
1702
|
+
imageExtensions = DEFAULT_IMAGE_EXTENSIONS,
|
|
1703
|
+
allowRootFiles = false,
|
|
1704
|
+
expectedSubfolders = []
|
|
1705
|
+
} = options;
|
|
1706
|
+
const errors = [];
|
|
1707
|
+
const warnings = [];
|
|
1708
|
+
const rootFiles = [];
|
|
1709
|
+
const subfolders = [];
|
|
1710
|
+
const lowerExts = imageExtensions.map((e) => e.toLowerCase());
|
|
1711
|
+
let entries;
|
|
1712
|
+
try {
|
|
1713
|
+
entries = await fsp.readdir(rootDir, { withFileTypes: true, encoding: "utf8" });
|
|
1714
|
+
} catch (err) {
|
|
1715
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1716
|
+
return {
|
|
1717
|
+
valid: false,
|
|
1718
|
+
errors: [`Could not read directory "${rootDir}": ${msg}`],
|
|
1719
|
+
warnings,
|
|
1720
|
+
rootFiles,
|
|
1721
|
+
subfolders
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
for (const entry of entries) {
|
|
1725
|
+
if (entry.isDirectory()) {
|
|
1726
|
+
subfolders.push(entry.name);
|
|
1727
|
+
} else if (entry.isFile()) {
|
|
1728
|
+
if (lowerExts.includes(extname(entry.name).toLowerCase())) {
|
|
1729
|
+
rootFiles.push(entry.name);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
if (!allowRootFiles && rootFiles.length > 0) {
|
|
1734
|
+
errors.push(
|
|
1735
|
+
`Image files found directly in "${rootDir}" \u2014 the folder structure may have been flattened during sync. Files: ${rootFiles.slice(0, 5).join(", ")}${rootFiles.length > 5 ? ` \u2026 (+${rootFiles.length - 5} more)` : ""}`
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
if (!allowRootFiles && subfolders.length === 0) {
|
|
1739
|
+
errors.push(
|
|
1740
|
+
`No sub-directories found in "${rootDir}". Expected a nested folder structure (e.g. projects/, performances/).`
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
const missing = expectedSubfolders.filter((name) => !subfolders.includes(name));
|
|
1744
|
+
if (missing.length > 0) {
|
|
1745
|
+
warnings.push(
|
|
1746
|
+
`Some expected sub-folders are absent from "${rootDir}": ${missing.join(", ")}`
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
return {
|
|
1750
|
+
valid: errors.length === 0,
|
|
1751
|
+
errors,
|
|
1752
|
+
warnings,
|
|
1753
|
+
rootFiles,
|
|
1754
|
+
subfolders
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// src/utils/getDriveTranslations.ts
|
|
1759
|
+
import path6 from "node:path";
|
|
1760
|
+
|
|
1761
|
+
// src/utils/driveProjectIndex.ts
|
|
1762
|
+
import fs6 from "node:fs";
|
|
1763
|
+
import path5 from "node:path";
|
|
1764
|
+
function buildManifest(options) {
|
|
1765
|
+
const locales = Object.keys(options.translations).sort();
|
|
1766
|
+
return {
|
|
1767
|
+
version: "1",
|
|
1768
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1769
|
+
projectName: options.projectName,
|
|
1770
|
+
domain: options.domain,
|
|
1771
|
+
locales,
|
|
1772
|
+
defaultLocale: options.defaultLocale,
|
|
1773
|
+
spreadsheets: options.spreadsheets,
|
|
1774
|
+
outputDirectory: options.outputDirectory,
|
|
1775
|
+
flatten: options.flatten,
|
|
1776
|
+
projectMetadata: options.projectMetadata
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
function writeManifest(manifest, manifestPath) {
|
|
1780
|
+
const dir = path5.dirname(manifestPath);
|
|
1781
|
+
if (!fs6.existsSync(dir)) {
|
|
1782
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
1783
|
+
}
|
|
1784
|
+
fs6.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
1785
|
+
console.log(`[driveProjectIndex] Wrote project manifest \u2192 ${manifestPath}`);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// src/utils/getDriveTranslations.ts
|
|
1789
|
+
function sanitizeFolderName(name) {
|
|
1790
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "sheet";
|
|
1791
|
+
}
|
|
1792
|
+
async function manageDriveTranslations(options) {
|
|
1793
|
+
const {
|
|
1794
|
+
driveFolderId,
|
|
1795
|
+
scanForSpreadsheets = true,
|
|
1796
|
+
spreadsheetIds: explicitIds = [],
|
|
1797
|
+
spreadsheetNameFilter,
|
|
1798
|
+
syncImages = false,
|
|
1799
|
+
imageOutputPath,
|
|
1800
|
+
imageSyncOptions,
|
|
1801
|
+
translationOptions = {},
|
|
1802
|
+
docTitles,
|
|
1803
|
+
flatten = true,
|
|
1804
|
+
createManifest,
|
|
1805
|
+
manifestPath,
|
|
1806
|
+
projectName,
|
|
1807
|
+
domain,
|
|
1808
|
+
defaultLocale,
|
|
1809
|
+
projectMetadata
|
|
1810
|
+
} = options;
|
|
1811
|
+
if (syncImages && !imageOutputPath) {
|
|
1812
|
+
throw new Error(
|
|
1813
|
+
"[manageDriveTranslations] imageOutputPath is required when syncImages is true"
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
const shouldCreateManifest = createManifest ?? driveFolderId !== void 0;
|
|
1817
|
+
const discoveredIds = [];
|
|
1818
|
+
const discoveredNames = /* @__PURE__ */ new Map();
|
|
1819
|
+
const discoveredFolderPaths = /* @__PURE__ */ new Map();
|
|
1820
|
+
const discoveredModifiedTimes = /* @__PURE__ */ new Map();
|
|
1821
|
+
if (driveFolderId && scanForSpreadsheets) {
|
|
1822
|
+
const scanOptions = { folderId: driveFolderId };
|
|
1823
|
+
const discovered = await scanDriveFolderForSpreadsheets(scanOptions);
|
|
1824
|
+
console.log(
|
|
1825
|
+
`[manageDriveTranslations] Found ${discovered.length} spreadsheet(s) in Drive folder`
|
|
1826
|
+
);
|
|
1827
|
+
for (const file of discovered) {
|
|
1828
|
+
discoveredIds.push(file.id);
|
|
1829
|
+
discoveredNames.set(file.id, file.name);
|
|
1830
|
+
discoveredFolderPaths.set(file.id, file.folderPath);
|
|
1831
|
+
if (file.modifiedTime) discoveredModifiedTimes.set(file.id, file.modifiedTime);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
const allIds = [.../* @__PURE__ */ new Set([...discoveredIds, ...explicitIds])];
|
|
1835
|
+
const filteredIds = spreadsheetNameFilter ? allIds.filter((id) => {
|
|
1836
|
+
const name = discoveredNames.get(id);
|
|
1837
|
+
if (!name) return true;
|
|
1838
|
+
return spreadsheetNameFilter.test(name);
|
|
1839
|
+
}) : allIds;
|
|
1840
|
+
let translations;
|
|
1841
|
+
const spreadsheetEntries = [];
|
|
1842
|
+
const baseOutputDir = translationOptions.translationsOutputDir ?? "translations";
|
|
1843
|
+
if (!flatten) {
|
|
1844
|
+
const { mergeStrategy = "later-wins", ...baseOptions } = translationOptions;
|
|
1845
|
+
const perResults = [];
|
|
1846
|
+
for (const id of filteredIds) {
|
|
1847
|
+
const name = discoveredNames.get(id) ?? id;
|
|
1848
|
+
const subDir = sanitizeFolderName(name);
|
|
1849
|
+
const subOutputDir = path6.join(baseOutputDir, subDir);
|
|
1850
|
+
console.log(
|
|
1851
|
+
`[manageDriveTranslations] (flatten: false) Fetching "${name}" \u2192 ${subOutputDir}`
|
|
1852
|
+
);
|
|
1853
|
+
const result = await getSpreadSheetData(docTitles, {
|
|
1854
|
+
...baseOptions,
|
|
1855
|
+
spreadsheetId: id,
|
|
1856
|
+
translationsOutputDir: subOutputDir
|
|
1857
|
+
});
|
|
1858
|
+
perResults.push(result);
|
|
1859
|
+
spreadsheetEntries.push({
|
|
1860
|
+
id,
|
|
1861
|
+
name,
|
|
1862
|
+
folderPath: discoveredFolderPaths.get(id) ?? "",
|
|
1863
|
+
sheets: docTitles ?? [],
|
|
1864
|
+
modifiedTime: discoveredModifiedTimes.get(id),
|
|
1865
|
+
outputSubDirectory: subDir
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
translations = mergeMultipleTranslationData(perResults, mergeStrategy);
|
|
1869
|
+
} else {
|
|
1870
|
+
translations = await getMultipleSpreadSheetsData(docTitles, {
|
|
1871
|
+
...translationOptions,
|
|
1872
|
+
spreadsheetIds: filteredIds.length > 0 ? filteredIds : void 0
|
|
1873
|
+
});
|
|
1874
|
+
for (const id of filteredIds) {
|
|
1875
|
+
spreadsheetEntries.push({
|
|
1876
|
+
id,
|
|
1877
|
+
name: discoveredNames.get(id) ?? id,
|
|
1878
|
+
folderPath: discoveredFolderPaths.get(id) ?? "",
|
|
1879
|
+
sheets: docTitles ?? [],
|
|
1880
|
+
modifiedTime: discoveredModifiedTimes.get(id)
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
let imageSync;
|
|
1885
|
+
if (syncImages && driveFolderId && imageOutputPath) {
|
|
1886
|
+
imageSync = await syncDriveImages({
|
|
1887
|
+
...imageSyncOptions,
|
|
1888
|
+
folderId: driveFolderId,
|
|
1889
|
+
outputPath: imageOutputPath
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
let manifest;
|
|
1893
|
+
if (shouldCreateManifest) {
|
|
1894
|
+
const resolvedManifestPath = manifestPath ?? path6.join(baseOutputDir, "i18n-manifest.json");
|
|
1895
|
+
manifest = buildManifest({
|
|
1896
|
+
translations,
|
|
1897
|
+
spreadsheets: spreadsheetEntries,
|
|
1898
|
+
outputDirectory: baseOutputDir,
|
|
1899
|
+
flatten,
|
|
1900
|
+
projectName,
|
|
1901
|
+
domain,
|
|
1902
|
+
defaultLocale,
|
|
1903
|
+
projectMetadata
|
|
1904
|
+
});
|
|
1905
|
+
writeManifest(manifest, resolvedManifestPath);
|
|
1906
|
+
}
|
|
1907
|
+
return { translations, spreadsheetIds: filteredIds, imageSync, manifest };
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1320
1910
|
// src/index.ts
|
|
1321
1911
|
var index_default = getSpreadSheetData;
|
|
1322
1912
|
export {
|
|
1913
|
+
DEFAULT_IMAGE_EXTENSIONS,
|
|
1323
1914
|
DEFAULT_WAIT_SECONDS,
|
|
1915
|
+
buildManifest,
|
|
1324
1916
|
convertFromDataJsonFormat,
|
|
1325
1917
|
convertToDataJsonFormat,
|
|
1326
1918
|
createAuthClient,
|
|
@@ -1332,23 +1924,32 @@ export {
|
|
|
1332
1924
|
getGoogleTranslateCode,
|
|
1333
1925
|
getLanguagePrefix,
|
|
1334
1926
|
getLocaleDisplayName,
|
|
1927
|
+
getMultipleSpreadSheetsData,
|
|
1335
1928
|
getNormalizedLocaleForHeader,
|
|
1336
1929
|
getOriginalHeaderForLocale,
|
|
1337
1930
|
getSpreadSheetData,
|
|
1338
1931
|
getTranslationSummary,
|
|
1339
1932
|
handleBidirectionalSync,
|
|
1340
1933
|
isValidLocale,
|
|
1934
|
+
manageDriveTranslations,
|
|
1935
|
+
mergeMultipleTranslationData,
|
|
1341
1936
|
mergeSheets,
|
|
1937
|
+
normalizeExtension,
|
|
1342
1938
|
normalizeLocaleCode,
|
|
1343
1939
|
processRawRows,
|
|
1344
1940
|
readPublicSheet,
|
|
1345
1941
|
resolveLocaleWithFallback,
|
|
1942
|
+
scanDriveFolderForSpreadsheets,
|
|
1943
|
+
syncDriveImages,
|
|
1346
1944
|
updateSpreadsheetWithLocalChanges,
|
|
1347
1945
|
validateCredentials,
|
|
1348
1946
|
validateEnv,
|
|
1947
|
+
validateImageDirectory,
|
|
1349
1948
|
wait,
|
|
1949
|
+
walkDirectory,
|
|
1350
1950
|
withRetry,
|
|
1351
1951
|
writeLanguageDataFile,
|
|
1352
1952
|
writeLocalesFile,
|
|
1953
|
+
writeManifest,
|
|
1353
1954
|
writeTranslationFiles
|
|
1354
1955
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TranslationData } from './types';
|
|
2
|
+
import type { SpreadsheetOptions } from './utils/configurationHandler';
|
|
3
|
+
export interface MultiSpreadsheetOptions extends SpreadsheetOptions {
|
|
4
|
+
/** Array of spreadsheet IDs to fetch from. Overrides spreadsheetId if provided. */
|
|
5
|
+
spreadsheetIds?: string[];
|
|
6
|
+
/**
|
|
7
|
+
* How to merge same-locale same-sheet keys from different spreadsheets.
|
|
8
|
+
* 'later-wins': keys from later spreadsheets override earlier (default)
|
|
9
|
+
* 'first-wins': keep first occurrence of each key
|
|
10
|
+
*/
|
|
11
|
+
mergeStrategy?: 'later-wins' | 'first-wins';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Fetches translations from multiple Google Spreadsheets and merges them.
|
|
15
|
+
* When spreadsheetIds is not provided, falls back to options.spreadsheetId
|
|
16
|
+
* or GOOGLE_SPREADSHEET_ID env var (same as getSpreadSheetData).
|
|
17
|
+
*/
|
|
18
|
+
export declare function getMultipleSpreadSheetsData(docTitles?: string[], options?: MultiSpreadsheetOptions): Promise<TranslationData>;
|
|
19
|
+
//# sourceMappingURL=getMultipleSpreadSheetsData.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getMultipleSpreadSheetsData.d.ts","sourceRoot":"","sources":["../src/getMultipleSpreadSheetsData.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAIvE,MAAM,WAAW,uBAAwB,SAAQ,kBAAkB;IAClE,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;OAIG;IACH,aAAa,CAAC,EAAE,YAAY,GAAG,YAAY,CAAC;CAC5C;AAED;;;;GAIG;AACH,wBAAsB,2BAA2B,CAChD,SAAS,CAAC,EAAE,MAAM,EAAE,EACpB,OAAO,GAAE,uBAA4B,GACnC,OAAO,CAAC,eAAe,CAAC,CAmB1B"}
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,19 @@ export { writeTranslationFiles, writeLocalesFile, writeLanguageDataFile } from '
|
|
|
25
25
|
export { handleBidirectionalSync } from './utils/syncManager';
|
|
26
26
|
export type { SyncResult } from './utils/syncManager';
|
|
27
27
|
export type { TranslationData, TranslationValue, SheetRow, GoogleEnvVars, } from './types';
|
|
28
|
+
export { getMultipleSpreadSheetsData } from './getMultipleSpreadSheetsData';
|
|
29
|
+
export type { MultiSpreadsheetOptions } from './getMultipleSpreadSheetsData';
|
|
30
|
+
export { mergeMultipleTranslationData } from './utils/multiSpreadsheetMerger';
|
|
31
|
+
export { scanDriveFolderForSpreadsheets } from './utils/driveFolderScanner';
|
|
32
|
+
export type { DriveSpreadsheetFile, ScanDriveFolderOptions } from './utils/driveFolderScanner';
|
|
33
|
+
export { syncDriveImages, normalizeExtension } from './utils/driveImageSync';
|
|
34
|
+
export type { DriveImageSyncOptions, DriveImageSyncResult } from './utils/driveImageSync';
|
|
35
|
+
export { walkDirectory, validateImageDirectory, DEFAULT_IMAGE_EXTENSIONS } from './utils/localImageUtils';
|
|
36
|
+
export type { WalkDirectoryOptions, ImageDirectoryValidationOptions, ImageDirectoryValidationResult, } from './utils/localImageUtils';
|
|
37
|
+
export { manageDriveTranslations } from './utils/getDriveTranslations';
|
|
38
|
+
export type { GoogleDriveManagerOptions, GoogleDriveManagerResult } from './utils/getDriveTranslations';
|
|
39
|
+
export { buildManifest, writeManifest } from './utils/driveProjectIndex';
|
|
40
|
+
export type { DriveProjectManifest, SpreadsheetManifestEntry, BuildManifestOptions } from './utils/driveProjectIndex';
|
|
28
41
|
import { getSpreadSheetData } from './getSpreadSheetData';
|
|
29
42
|
export default getSpreadSheetData;
|
|
30
43
|
//# sourceMappingURL=index.d.ts.map
|