@el-j/google-sheet-translations 2.1.4 → 2.2.0-beta.1
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 -5
- package/dist/action-entrypoint.d.ts.map +1 -1
- package/dist/esm/index.js +404 -7
- package/dist/getMultipleSpreadSheetsData.d.ts +19 -0
- package/dist/getMultipleSpreadSheetsData.d.ts.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +410 -7
- package/dist/utils/driveFolderScanner.d.ts +26 -0
- package/dist/utils/driveFolderScanner.d.ts.map +1 -0
- package/dist/utils/driveImageSync.d.ts +44 -0
- package/dist/utils/driveImageSync.d.ts.map +1 -0
- package/dist/utils/getDriveTranslations.d.ts +80 -0
- package/dist/utils/getDriveTranslations.d.ts.map +1 -0
- package/dist/utils/localeNormalizer.d.ts +22 -0
- package/dist/utils/localeNormalizer.d.ts.map +1 -1
- package/dist/utils/multiSpreadsheetMerger.d.ts +7 -0
- package/dist/utils/multiSpreadsheetMerger.d.ts.map +1 -0
- package/dist/utils/spreadsheetCreator.d.ts.map +1 -1
- package/dist/utils/spreadsheetUpdater.d.ts +5 -4
- package/dist/utils/spreadsheetUpdater.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -245,18 +245,19 @@ When enabled, the auto-translation feature automatically adds Google Translate f
|
|
|
245
245
|
|
|
246
246
|
The formula follows this format:
|
|
247
247
|
```
|
|
248
|
-
=GOOGLETRANSLATE(INDIRECT(
|
|
248
|
+
=GOOGLETRANSLATE(INDIRECT(srcCol&ROW()); langCodeFormula($srcCol$1); langCodeFormula(tgtCol$1))
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
-
|
|
251
|
+
The `langCodeFormula` dynamically extracts the GOOGLETRANSLATE-compatible language code from the header cell — stripping region qualifiers (e.g. `"tr-TR"` → `"tr"`) while preserving Chinese variants (`zh-TW` / `zh-CN`). All formula separators are automatically matched to the spreadsheet's locale.
|
|
252
|
+
|
|
253
|
+
For example, if you add a new key with an English translation in column B but no Turkish translation (header `tr-TR`) in column C, the system will automatically add:
|
|
252
254
|
```
|
|
253
|
-
=GOOGLETRANSLATE(INDIRECT("B"&ROW());$B$1;C$1)
|
|
255
|
+
=GOOGLETRANSLATE(INDIRECT("B"&ROW());IF(LOWER(LEFT($B$1;3))="zh-";LOWER($B$1);LOWER(IFERROR(LEFT($B$1;FIND("-";$B$1)-1);$B$1)));IF(LOWER(LEFT(C$1;3))="zh-";LOWER(C$1);LOWER(IFERROR(LEFT(C$1;FIND("-";C$1)-1);C$1))))
|
|
254
256
|
```
|
|
255
257
|
|
|
256
258
|
Where:
|
|
257
259
|
- `INDIRECT("B"&ROW())` dynamically references the source text cell in the same row
|
|
258
|
-
- `$B$1`
|
|
259
|
-
- `C$1` references the header cell containing the target language code
|
|
260
|
+
- Each `IF(…)` wrapper extracts the language code from the header cell (`$B$1` / `C$1`), producing `"en"` and `"tr"` respectively at evaluation time
|
|
260
261
|
|
|
261
262
|
[View the complete Auto-Translation Guide](docs/auto-translation-guide.md) for more details and best practices.
|
|
262
263
|
|
|
@@ -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
|
@@ -169,6 +169,14 @@ function filterValidLocales(headerRow, keyColumn) {
|
|
|
169
169
|
function getLanguagePrefix(locale) {
|
|
170
170
|
return locale.toLowerCase().split(/[-_]/)[0];
|
|
171
171
|
}
|
|
172
|
+
var GOOGLE_TRANSLATE_CODES_REQUIRING_REGION = /* @__PURE__ */ new Set(["zh-tw", "zh-cn"]);
|
|
173
|
+
function getGoogleTranslateCode(locale) {
|
|
174
|
+
const normalized = locale.toLowerCase().trim().replace("_", "-");
|
|
175
|
+
if (GOOGLE_TRANSLATE_CODES_REQUIRING_REGION.has(normalized)) {
|
|
176
|
+
return normalized;
|
|
177
|
+
}
|
|
178
|
+
return normalized.split(/[-_]/)[0];
|
|
179
|
+
}
|
|
172
180
|
var LANGUAGE_TO_COUNTRY_MAP = {
|
|
173
181
|
"en": "en-GB",
|
|
174
182
|
"de": "de-DE",
|
|
@@ -531,12 +539,23 @@ function columnIndexToLetter(index) {
|
|
|
531
539
|
} while (i >= 0);
|
|
532
540
|
return result;
|
|
533
541
|
}
|
|
534
|
-
function
|
|
535
|
-
|
|
542
|
+
function getFormulaSeparator(doc) {
|
|
543
|
+
try {
|
|
544
|
+
const locale = doc._rawProperties?.locale || "";
|
|
545
|
+
if (/^(en|ja|ko|zh|th|id|ms)/i.test(locale)) return ",";
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
return ";";
|
|
549
|
+
}
|
|
550
|
+
function langCodeFormula(cellRef, sep) {
|
|
551
|
+
const prefix = `LOWER(IFERROR(LEFT(${cellRef}${sep}FIND("-"${sep}${cellRef})-1)${sep}${cellRef}))`;
|
|
552
|
+
const full = `LOWER(${cellRef})`;
|
|
553
|
+
return `IF(LOWER(LEFT(${cellRef}${sep}3))="zh-"${sep}${full}${sep}${prefix})`;
|
|
536
554
|
}
|
|
537
555
|
async function updateSpreadsheetWithLocalChanges(doc, changes, waitSeconds, autoTranslate = false, localeMapping = {}, override = false) {
|
|
538
556
|
console.log("Updating spreadsheet with local changes...");
|
|
539
557
|
const baseDelayMs = waitSeconds * 1e3;
|
|
558
|
+
const sep = getFormulaSeparator(doc);
|
|
540
559
|
for (const sheetTitle of new Set(
|
|
541
560
|
Object.values(changes).flatMap((locale) => Object.keys(locale))
|
|
542
561
|
)) {
|
|
@@ -680,7 +699,7 @@ async function updateSpreadsheetWithLocalChanges(doc, changes, waitSeconds, auto
|
|
|
680
699
|
const targetColumnLetter = columnIndexToLetter(targetHeaderIndex);
|
|
681
700
|
row.set(
|
|
682
701
|
exactTargetHeader,
|
|
683
|
-
`=GOOGLETRANSLATE(INDIRECT("${sourceColumnLetter}"&ROW())
|
|
702
|
+
`=GOOGLETRANSLATE(INDIRECT("${sourceColumnLetter}"&ROW())${sep}${langCodeFormula(`$${sourceColumnLetter}$1`, sep)}${sep}${langCodeFormula(`${targetColumnLetter}$1`, sep)})`
|
|
684
703
|
);
|
|
685
704
|
}
|
|
686
705
|
}
|
|
@@ -729,7 +748,7 @@ async function updateSpreadsheetWithLocalChanges(doc, changes, waitSeconds, auto
|
|
|
729
748
|
}
|
|
730
749
|
const sourceColumnLetter = columnIndexToLetter(sourceHeaderIndex);
|
|
731
750
|
const targetColumnLetter = columnIndexToLetter(targetHeaderIndex);
|
|
732
|
-
rowData[exactHeaderName] = `=GOOGLETRANSLATE(INDIRECT("${sourceColumnLetter}"&ROW())
|
|
751
|
+
rowData[exactHeaderName] = `=GOOGLETRANSLATE(INDIRECT("${sourceColumnLetter}"&ROW())${sep}${langCodeFormula(`$${sourceColumnLetter}$1`, sep)}${sep}${langCodeFormula(`${targetColumnLetter}$1`, sep)})`;
|
|
733
752
|
}
|
|
734
753
|
}
|
|
735
754
|
}
|
|
@@ -985,8 +1004,18 @@ function colLetter(index) {
|
|
|
985
1004
|
} while (i >= 0);
|
|
986
1005
|
return result;
|
|
987
1006
|
}
|
|
988
|
-
function
|
|
989
|
-
|
|
1007
|
+
function getFormulaSeparator2(doc) {
|
|
1008
|
+
try {
|
|
1009
|
+
const locale = doc._rawProperties?.locale || "";
|
|
1010
|
+
if (/^(en|ja|ko|zh|th|id|ms)/i.test(locale)) return ",";
|
|
1011
|
+
} catch {
|
|
1012
|
+
}
|
|
1013
|
+
return ";";
|
|
1014
|
+
}
|
|
1015
|
+
function langCodeFormula2(cellRef, sep) {
|
|
1016
|
+
const prefix = `LOWER(IFERROR(LEFT(${cellRef}${sep}FIND("-"${sep}${cellRef})-1)${sep}${cellRef}))`;
|
|
1017
|
+
const full = `LOWER(${cellRef})`;
|
|
1018
|
+
return `IF(LOWER(LEFT(${cellRef}${sep}3))="zh-"${sep}${full}${sep}${prefix})`;
|
|
990
1019
|
}
|
|
991
1020
|
var DEFAULT_TARGET_LOCALES = ["de", "fr", "es", "it", "pt", "ja", "zh"];
|
|
992
1021
|
var STARTER_KEYS = {
|
|
@@ -1068,11 +1097,12 @@ async function createSpreadsheet(authClient, options = {}) {
|
|
|
1068
1097
|
"setHeaderRow"
|
|
1069
1098
|
);
|
|
1070
1099
|
const sourceColLetter = colLetter(1);
|
|
1100
|
+
const sep = getFormulaSeparator2(doc);
|
|
1071
1101
|
const rows = Object.entries(seedKeys).map(([key, sourceValue]) => {
|
|
1072
1102
|
const row = { key, [sourceLocale]: sourceValue };
|
|
1073
1103
|
targetLocales.forEach((locale, idx) => {
|
|
1074
1104
|
const targetColLetter = colLetter(2 + idx);
|
|
1075
|
-
row[locale] = `=GOOGLETRANSLATE(INDIRECT("${sourceColLetter}"&ROW())
|
|
1105
|
+
row[locale] = `=GOOGLETRANSLATE(INDIRECT("${sourceColLetter}"&ROW())${sep}${langCodeFormula2(`$${sourceColLetter}$1`, sep)}${sep}${langCodeFormula2(`${targetColLetter}$1`, sep)})`;
|
|
1076
1106
|
});
|
|
1077
1107
|
return row;
|
|
1078
1108
|
});
|
|
@@ -1287,6 +1317,367 @@ function mergeSheets(translations, locale, sheetNames) {
|
|
|
1287
1317
|
return merged;
|
|
1288
1318
|
}
|
|
1289
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
|
+
async function getAccessToken2(credentials) {
|
|
1470
|
+
const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
|
|
1471
|
+
const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
|
|
1472
|
+
if (!clientEmail || !privateKey) {
|
|
1473
|
+
throw new Error(
|
|
1474
|
+
"Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
const normalizedKey = privateKey.replace(/\\n/g, "\n");
|
|
1478
|
+
const auth = new GoogleAuth2({
|
|
1479
|
+
credentials: { client_email: clientEmail, private_key: normalizedKey },
|
|
1480
|
+
scopes: ["https://www.googleapis.com/auth/drive.readonly"]
|
|
1481
|
+
});
|
|
1482
|
+
const client = await auth.getClient();
|
|
1483
|
+
const tokenResponse = await client.getAccessToken();
|
|
1484
|
+
return tokenResponse.token;
|
|
1485
|
+
}
|
|
1486
|
+
async function listFilesInFolder2(folderId, token, mimeTypeFilter) {
|
|
1487
|
+
const results = [];
|
|
1488
|
+
let pageToken;
|
|
1489
|
+
do {
|
|
1490
|
+
const mimeClause = mimeTypeFilter ? ` and mimeType = '${mimeTypeFilter}'` : "";
|
|
1491
|
+
const query = `'${folderId}' in parents${mimeClause} and trashed = false`;
|
|
1492
|
+
const params = new URLSearchParams({
|
|
1493
|
+
q: query,
|
|
1494
|
+
fields: "nextPageToken,files(id,name,mimeType,parents)",
|
|
1495
|
+
pageSize: "1000"
|
|
1496
|
+
});
|
|
1497
|
+
if (pageToken) params.set("pageToken", pageToken);
|
|
1498
|
+
const response = await fetch(`${DRIVE_FILES_URL2}?${params.toString()}`, {
|
|
1499
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1500
|
+
});
|
|
1501
|
+
if (!response.ok) {
|
|
1502
|
+
const text = await response.text();
|
|
1503
|
+
throw new Error(`Drive API error ${response.status}: ${text}`);
|
|
1504
|
+
}
|
|
1505
|
+
const data = await response.json();
|
|
1506
|
+
results.push(...data.files);
|
|
1507
|
+
pageToken = data.nextPageToken;
|
|
1508
|
+
} while (pageToken);
|
|
1509
|
+
return results;
|
|
1510
|
+
}
|
|
1511
|
+
async function collectFiles(folderId, folderRelPath, outputPath, token, allowedMimeTypes, recursive, folderPattern) {
|
|
1512
|
+
console.log(`[driveImageSync] Scanning folder: ${folderId} (path: "${folderRelPath}")`);
|
|
1513
|
+
const allItems = await listFilesInFolder2(folderId, token);
|
|
1514
|
+
const entries = [];
|
|
1515
|
+
for (const item of allItems) {
|
|
1516
|
+
if (item.mimeType === FOLDER_MIME2) {
|
|
1517
|
+
if (!recursive) continue;
|
|
1518
|
+
const subRelPath = folderRelPath ? `${folderRelPath}/${item.name}` : item.name;
|
|
1519
|
+
if (folderPattern && !folderPattern.test(subRelPath)) continue;
|
|
1520
|
+
const subEntries = await collectFiles(
|
|
1521
|
+
item.id,
|
|
1522
|
+
subRelPath,
|
|
1523
|
+
outputPath,
|
|
1524
|
+
token,
|
|
1525
|
+
allowedMimeTypes,
|
|
1526
|
+
recursive,
|
|
1527
|
+
folderPattern
|
|
1528
|
+
);
|
|
1529
|
+
entries.push(...subEntries);
|
|
1530
|
+
} else if (allowedMimeTypes.includes(item.mimeType)) {
|
|
1531
|
+
const localPath = folderRelPath ? join(outputPath, folderRelPath, item.name) : join(outputPath, item.name);
|
|
1532
|
+
entries.push({ id: item.id, name: item.name, localPath, mimeType: item.mimeType });
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return entries;
|
|
1536
|
+
}
|
|
1537
|
+
async function downloadFile(fileId, localPath, token) {
|
|
1538
|
+
const url = `${DRIVE_FILES_URL2}/${fileId}?alt=media`;
|
|
1539
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
1540
|
+
if (!response.ok) {
|
|
1541
|
+
throw new Error(`Failed to download ${fileId}: ${response.status}`);
|
|
1542
|
+
}
|
|
1543
|
+
mkdirSync(dirname(localPath), { recursive: true });
|
|
1544
|
+
const dest = createWriteStream(localPath);
|
|
1545
|
+
await pipeline(Readable.fromWeb(response.body), dest);
|
|
1546
|
+
}
|
|
1547
|
+
function collectLocalFiles(dir, base) {
|
|
1548
|
+
const results = [];
|
|
1549
|
+
if (!existsSync(dir)) return results;
|
|
1550
|
+
for (const entry of readdirSync(dir)) {
|
|
1551
|
+
const fullPath = join(dir, entry);
|
|
1552
|
+
const stat = statSync(fullPath);
|
|
1553
|
+
if (stat.isDirectory()) {
|
|
1554
|
+
results.push(...collectLocalFiles(fullPath, base));
|
|
1555
|
+
} else {
|
|
1556
|
+
results.push(fullPath);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return results;
|
|
1560
|
+
}
|
|
1561
|
+
async function runConcurrent(tasks, concurrency) {
|
|
1562
|
+
const results = [];
|
|
1563
|
+
for (let i = 0; i < tasks.length; i += concurrency) {
|
|
1564
|
+
const batch = tasks.slice(i, i + concurrency).map((t) => t());
|
|
1565
|
+
results.push(...await Promise.all(batch));
|
|
1566
|
+
}
|
|
1567
|
+
return results;
|
|
1568
|
+
}
|
|
1569
|
+
async function syncDriveImages(options) {
|
|
1570
|
+
const {
|
|
1571
|
+
folderId,
|
|
1572
|
+
outputPath,
|
|
1573
|
+
mimeTypes = DEFAULT_IMAGE_MIME_TYPES,
|
|
1574
|
+
recursive = true,
|
|
1575
|
+
folderPattern,
|
|
1576
|
+
credentials,
|
|
1577
|
+
cleanSync = false,
|
|
1578
|
+
concurrency = 3
|
|
1579
|
+
} = options;
|
|
1580
|
+
const token = await getAccessToken2(credentials);
|
|
1581
|
+
mkdirSync(outputPath, { recursive: true });
|
|
1582
|
+
const entries = await collectFiles(
|
|
1583
|
+
folderId,
|
|
1584
|
+
"",
|
|
1585
|
+
outputPath,
|
|
1586
|
+
token,
|
|
1587
|
+
mimeTypes,
|
|
1588
|
+
recursive,
|
|
1589
|
+
folderPattern
|
|
1590
|
+
);
|
|
1591
|
+
const downloaded = [];
|
|
1592
|
+
const skipped = [];
|
|
1593
|
+
const errors = [];
|
|
1594
|
+
const tasks = entries.map((entry) => async () => {
|
|
1595
|
+
if (existsSync(entry.localPath)) {
|
|
1596
|
+
console.log(`[driveImageSync] Skipping (exists): ${entry.localPath}`);
|
|
1597
|
+
skipped.push(entry.localPath);
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
console.log(`[driveImageSync] Downloading: ${entry.localPath}`);
|
|
1601
|
+
try {
|
|
1602
|
+
await downloadFile(entry.id, entry.localPath, token);
|
|
1603
|
+
downloaded.push(entry.localPath);
|
|
1604
|
+
} catch (err) {
|
|
1605
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1606
|
+
console.error(`[driveImageSync] Error downloading ${entry.localPath}: ${msg}`);
|
|
1607
|
+
errors.push(entry.localPath);
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
await runConcurrent(tasks, concurrency);
|
|
1611
|
+
const deleted = [];
|
|
1612
|
+
if (cleanSync) {
|
|
1613
|
+
const driveLocalPaths = new Set(entries.map((e) => e.localPath));
|
|
1614
|
+
const localFiles = collectLocalFiles(outputPath, outputPath);
|
|
1615
|
+
for (const localFile of localFiles) {
|
|
1616
|
+
if (!driveLocalPaths.has(localFile)) {
|
|
1617
|
+
console.log(`[driveImageSync] Deleting (not in Drive): ${localFile}`);
|
|
1618
|
+
unlinkSync(localFile);
|
|
1619
|
+
deleted.push(localFile);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
console.log(
|
|
1624
|
+
`[driveImageSync] Synced ${downloaded.length} files, skipped ${skipped.length}, deleted ${deleted.length}, errors ${errors.length}`
|
|
1625
|
+
);
|
|
1626
|
+
return { downloaded, skipped, deleted, errors };
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// src/utils/getDriveTranslations.ts
|
|
1630
|
+
async function manageDriveTranslations(options) {
|
|
1631
|
+
const {
|
|
1632
|
+
driveFolderId,
|
|
1633
|
+
scanForSpreadsheets = true,
|
|
1634
|
+
spreadsheetIds: explicitIds = [],
|
|
1635
|
+
spreadsheetNameFilter,
|
|
1636
|
+
syncImages = false,
|
|
1637
|
+
imageOutputPath,
|
|
1638
|
+
imageSyncOptions,
|
|
1639
|
+
translationOptions = {},
|
|
1640
|
+
docTitles
|
|
1641
|
+
} = options;
|
|
1642
|
+
if (syncImages && !imageOutputPath) {
|
|
1643
|
+
throw new Error(
|
|
1644
|
+
"[manageDriveTranslations] imageOutputPath is required when syncImages is true"
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
const discoveredIds = [];
|
|
1648
|
+
const discoveredNames = /* @__PURE__ */ new Map();
|
|
1649
|
+
if (driveFolderId && scanForSpreadsheets) {
|
|
1650
|
+
const scanOptions = { folderId: driveFolderId };
|
|
1651
|
+
const discovered = await scanDriveFolderForSpreadsheets(scanOptions);
|
|
1652
|
+
console.log(
|
|
1653
|
+
`[manageDriveTranslations] Found ${discovered.length} spreadsheet(s) in Drive folder`
|
|
1654
|
+
);
|
|
1655
|
+
for (const file of discovered) {
|
|
1656
|
+
discoveredIds.push(file.id);
|
|
1657
|
+
discoveredNames.set(file.id, file.name);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const allIds = [.../* @__PURE__ */ new Set([...discoveredIds, ...explicitIds])];
|
|
1661
|
+
const filteredIds = spreadsheetNameFilter ? allIds.filter((id) => {
|
|
1662
|
+
const name = discoveredNames.get(id);
|
|
1663
|
+
if (!name) return true;
|
|
1664
|
+
return spreadsheetNameFilter.test(name);
|
|
1665
|
+
}) : allIds;
|
|
1666
|
+
const translations = await getMultipleSpreadSheetsData(docTitles, {
|
|
1667
|
+
...translationOptions,
|
|
1668
|
+
spreadsheetIds: filteredIds.length > 0 ? filteredIds : void 0
|
|
1669
|
+
});
|
|
1670
|
+
let imageSync;
|
|
1671
|
+
if (syncImages && driveFolderId && imageOutputPath) {
|
|
1672
|
+
imageSync = await syncDriveImages({
|
|
1673
|
+
...imageSyncOptions,
|
|
1674
|
+
folderId: driveFolderId,
|
|
1675
|
+
outputPath: imageOutputPath
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
return { translations, spreadsheetIds: filteredIds, imageSync };
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1290
1681
|
// src/index.ts
|
|
1291
1682
|
var index_default = getSpreadSheetData;
|
|
1292
1683
|
export {
|
|
@@ -1299,19 +1690,25 @@ export {
|
|
|
1299
1690
|
index_default as default,
|
|
1300
1691
|
filterValidLocales,
|
|
1301
1692
|
findLocalChanges,
|
|
1693
|
+
getGoogleTranslateCode,
|
|
1302
1694
|
getLanguagePrefix,
|
|
1303
1695
|
getLocaleDisplayName,
|
|
1696
|
+
getMultipleSpreadSheetsData,
|
|
1304
1697
|
getNormalizedLocaleForHeader,
|
|
1305
1698
|
getOriginalHeaderForLocale,
|
|
1306
1699
|
getSpreadSheetData,
|
|
1307
1700
|
getTranslationSummary,
|
|
1308
1701
|
handleBidirectionalSync,
|
|
1309
1702
|
isValidLocale,
|
|
1703
|
+
manageDriveTranslations,
|
|
1704
|
+
mergeMultipleTranslationData,
|
|
1310
1705
|
mergeSheets,
|
|
1311
1706
|
normalizeLocaleCode,
|
|
1312
1707
|
processRawRows,
|
|
1313
1708
|
readPublicSheet,
|
|
1314
1709
|
resolveLocaleWithFallback,
|
|
1710
|
+
scanDriveFolderForSpreadsheets,
|
|
1711
|
+
syncDriveImages,
|
|
1315
1712
|
updateSpreadsheetWithLocalChanges,
|
|
1316
1713
|
validateCredentials,
|
|
1317
1714
|
validateEnv,
|
|
@@ -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
|
@@ -16,7 +16,7 @@ export { readPublicSheet } from './utils/publicSheetReader';
|
|
|
16
16
|
export { createSpreadsheet } from './utils/spreadsheetCreator';
|
|
17
17
|
export { validateCredentials } from './utils/validateEnv';
|
|
18
18
|
export { isValidLocale, filterValidLocales } from './utils/localeFilter';
|
|
19
|
-
export { getLanguagePrefix, normalizeLocaleCode, createLocaleMapping, getOriginalHeaderForLocale, getNormalizedLocaleForHeader, resolveLocaleWithFallback, } from './utils/localeNormalizer';
|
|
19
|
+
export { getLanguagePrefix, getGoogleTranslateCode, normalizeLocaleCode, createLocaleMapping, getOriginalHeaderForLocale, getNormalizedLocaleForHeader, resolveLocaleWithFallback, } from './utils/localeNormalizer';
|
|
20
20
|
export { processRawRows } from './utils/sheetProcessor';
|
|
21
21
|
export type { SheetProcessingResult } from './utils/sheetProcessor';
|
|
22
22
|
export { getTranslationSummary, getLocaleDisplayName, mergeSheets } from './utils/translationHelpers';
|
|
@@ -25,6 +25,15 @@ 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 } from './utils/driveImageSync';
|
|
34
|
+
export type { DriveImageSyncOptions, DriveImageSyncResult } from './utils/driveImageSync';
|
|
35
|
+
export { manageDriveTranslations } from './utils/getDriveTranslations';
|
|
36
|
+
export type { GoogleDriveManagerOptions, GoogleDriveManagerResult } from './utils/getDriveTranslations';
|
|
28
37
|
import { getSpreadSheetData } from './getSpreadSheetData';
|
|
29
38
|
export default getSpreadSheetData;
|
|
30
39
|
//# 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,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,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,eAAe,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAGhF,YAAY,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAGvE,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,uBAAuB,EAAE,MAAM,+CAA+C,CAAC;AACxF,OAAO,EAAE,yBAAyB,EAAE,MAAM,iDAAiD,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAC;AAC1E,OAAO,EAAE,iCAAiC,EAAE,MAAM,4BAA4B,CAAC;AAG/E,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAGzE,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,0BAA0B,EAC1B,4BAA4B,EAC5B,yBAAyB,GAC1B,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAGpE,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACtG,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAGnF,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAGpG,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGtD,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,QAAQ,EACR,aAAa,GACd,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAC5E,YAAY,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAG9E,OAAO,EAAE,8BAA8B,EAAE,MAAM,4BAA4B,CAAC;AAC5E,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAG/F,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAG1F,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,YAAY,EAAE,yBAAyB,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAGxG,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,eAAe,kBAAkB,CAAC"}
|