@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/dist/index.js
CHANGED
|
@@ -39,19 +39,25 @@ __export(index_exports, {
|
|
|
39
39
|
default: () => index_default,
|
|
40
40
|
filterValidLocales: () => filterValidLocales,
|
|
41
41
|
findLocalChanges: () => findLocalChanges,
|
|
42
|
+
getGoogleTranslateCode: () => getGoogleTranslateCode,
|
|
42
43
|
getLanguagePrefix: () => getLanguagePrefix,
|
|
43
44
|
getLocaleDisplayName: () => getLocaleDisplayName,
|
|
45
|
+
getMultipleSpreadSheetsData: () => getMultipleSpreadSheetsData,
|
|
44
46
|
getNormalizedLocaleForHeader: () => getNormalizedLocaleForHeader,
|
|
45
47
|
getOriginalHeaderForLocale: () => getOriginalHeaderForLocale,
|
|
46
48
|
getSpreadSheetData: () => getSpreadSheetData,
|
|
47
49
|
getTranslationSummary: () => getTranslationSummary,
|
|
48
50
|
handleBidirectionalSync: () => handleBidirectionalSync,
|
|
49
51
|
isValidLocale: () => isValidLocale,
|
|
52
|
+
manageDriveTranslations: () => manageDriveTranslations,
|
|
53
|
+
mergeMultipleTranslationData: () => mergeMultipleTranslationData,
|
|
50
54
|
mergeSheets: () => mergeSheets,
|
|
51
55
|
normalizeLocaleCode: () => normalizeLocaleCode,
|
|
52
56
|
processRawRows: () => processRawRows,
|
|
53
57
|
readPublicSheet: () => readPublicSheet,
|
|
54
58
|
resolveLocaleWithFallback: () => resolveLocaleWithFallback,
|
|
59
|
+
scanDriveFolderForSpreadsheets: () => scanDriveFolderForSpreadsheets,
|
|
60
|
+
syncDriveImages: () => syncDriveImages,
|
|
55
61
|
updateSpreadsheetWithLocalChanges: () => updateSpreadsheetWithLocalChanges,
|
|
56
62
|
validateCredentials: () => validateCredentials,
|
|
57
63
|
validateEnv: () => validateEnv,
|
|
@@ -234,6 +240,14 @@ function filterValidLocales(headerRow, keyColumn) {
|
|
|
234
240
|
function getLanguagePrefix(locale) {
|
|
235
241
|
return locale.toLowerCase().split(/[-_]/)[0];
|
|
236
242
|
}
|
|
243
|
+
var GOOGLE_TRANSLATE_CODES_REQUIRING_REGION = /* @__PURE__ */ new Set(["zh-tw", "zh-cn"]);
|
|
244
|
+
function getGoogleTranslateCode(locale) {
|
|
245
|
+
const normalized = locale.toLowerCase().trim().replace("_", "-");
|
|
246
|
+
if (GOOGLE_TRANSLATE_CODES_REQUIRING_REGION.has(normalized)) {
|
|
247
|
+
return normalized;
|
|
248
|
+
}
|
|
249
|
+
return normalized.split(/[-_]/)[0];
|
|
250
|
+
}
|
|
237
251
|
var LANGUAGE_TO_COUNTRY_MAP = {
|
|
238
252
|
"en": "en-GB",
|
|
239
253
|
"de": "de-DE",
|
|
@@ -596,12 +610,23 @@ function columnIndexToLetter(index) {
|
|
|
596
610
|
} while (i >= 0);
|
|
597
611
|
return result;
|
|
598
612
|
}
|
|
599
|
-
function
|
|
600
|
-
|
|
613
|
+
function getFormulaSeparator(doc) {
|
|
614
|
+
try {
|
|
615
|
+
const locale = doc._rawProperties?.locale || "";
|
|
616
|
+
if (/^(en|ja|ko|zh|th|id|ms)/i.test(locale)) return ",";
|
|
617
|
+
} catch {
|
|
618
|
+
}
|
|
619
|
+
return ";";
|
|
620
|
+
}
|
|
621
|
+
function langCodeFormula(cellRef, sep) {
|
|
622
|
+
const prefix = `LOWER(IFERROR(LEFT(${cellRef}${sep}FIND("-"${sep}${cellRef})-1)${sep}${cellRef}))`;
|
|
623
|
+
const full = `LOWER(${cellRef})`;
|
|
624
|
+
return `IF(LOWER(LEFT(${cellRef}${sep}3))="zh-"${sep}${full}${sep}${prefix})`;
|
|
601
625
|
}
|
|
602
626
|
async function updateSpreadsheetWithLocalChanges(doc, changes, waitSeconds, autoTranslate = false, localeMapping = {}, override = false) {
|
|
603
627
|
console.log("Updating spreadsheet with local changes...");
|
|
604
628
|
const baseDelayMs = waitSeconds * 1e3;
|
|
629
|
+
const sep = getFormulaSeparator(doc);
|
|
605
630
|
for (const sheetTitle of new Set(
|
|
606
631
|
Object.values(changes).flatMap((locale) => Object.keys(locale))
|
|
607
632
|
)) {
|
|
@@ -745,7 +770,7 @@ async function updateSpreadsheetWithLocalChanges(doc, changes, waitSeconds, auto
|
|
|
745
770
|
const targetColumnLetter = columnIndexToLetter(targetHeaderIndex);
|
|
746
771
|
row.set(
|
|
747
772
|
exactTargetHeader,
|
|
748
|
-
`=GOOGLETRANSLATE(INDIRECT("${sourceColumnLetter}"&ROW())
|
|
773
|
+
`=GOOGLETRANSLATE(INDIRECT("${sourceColumnLetter}"&ROW())${sep}${langCodeFormula(`$${sourceColumnLetter}$1`, sep)}${sep}${langCodeFormula(`${targetColumnLetter}$1`, sep)})`
|
|
749
774
|
);
|
|
750
775
|
}
|
|
751
776
|
}
|
|
@@ -794,7 +819,7 @@ async function updateSpreadsheetWithLocalChanges(doc, changes, waitSeconds, auto
|
|
|
794
819
|
}
|
|
795
820
|
const sourceColumnLetter = columnIndexToLetter(sourceHeaderIndex);
|
|
796
821
|
const targetColumnLetter = columnIndexToLetter(targetHeaderIndex);
|
|
797
|
-
rowData[exactHeaderName] = `=GOOGLETRANSLATE(INDIRECT("${sourceColumnLetter}"&ROW())
|
|
822
|
+
rowData[exactHeaderName] = `=GOOGLETRANSLATE(INDIRECT("${sourceColumnLetter}"&ROW())${sep}${langCodeFormula(`$${sourceColumnLetter}$1`, sep)}${sep}${langCodeFormula(`${targetColumnLetter}$1`, sep)})`;
|
|
798
823
|
}
|
|
799
824
|
}
|
|
800
825
|
}
|
|
@@ -1050,8 +1075,18 @@ function colLetter(index) {
|
|
|
1050
1075
|
} while (i >= 0);
|
|
1051
1076
|
return result;
|
|
1052
1077
|
}
|
|
1053
|
-
function
|
|
1054
|
-
|
|
1078
|
+
function getFormulaSeparator2(doc) {
|
|
1079
|
+
try {
|
|
1080
|
+
const locale = doc._rawProperties?.locale || "";
|
|
1081
|
+
if (/^(en|ja|ko|zh|th|id|ms)/i.test(locale)) return ",";
|
|
1082
|
+
} catch {
|
|
1083
|
+
}
|
|
1084
|
+
return ";";
|
|
1085
|
+
}
|
|
1086
|
+
function langCodeFormula2(cellRef, sep) {
|
|
1087
|
+
const prefix = `LOWER(IFERROR(LEFT(${cellRef}${sep}FIND("-"${sep}${cellRef})-1)${sep}${cellRef}))`;
|
|
1088
|
+
const full = `LOWER(${cellRef})`;
|
|
1089
|
+
return `IF(LOWER(LEFT(${cellRef}${sep}3))="zh-"${sep}${full}${sep}${prefix})`;
|
|
1055
1090
|
}
|
|
1056
1091
|
var DEFAULT_TARGET_LOCALES = ["de", "fr", "es", "it", "pt", "ja", "zh"];
|
|
1057
1092
|
var STARTER_KEYS = {
|
|
@@ -1133,11 +1168,12 @@ async function createSpreadsheet(authClient, options = {}) {
|
|
|
1133
1168
|
"setHeaderRow"
|
|
1134
1169
|
);
|
|
1135
1170
|
const sourceColLetter = colLetter(1);
|
|
1171
|
+
const sep = getFormulaSeparator2(doc);
|
|
1136
1172
|
const rows = Object.entries(seedKeys).map(([key, sourceValue]) => {
|
|
1137
1173
|
const row = { key, [sourceLocale]: sourceValue };
|
|
1138
1174
|
targetLocales.forEach((locale, idx) => {
|
|
1139
1175
|
const targetColLetter = colLetter(2 + idx);
|
|
1140
|
-
row[locale] = `=GOOGLETRANSLATE(INDIRECT("${sourceColLetter}"&ROW())
|
|
1176
|
+
row[locale] = `=GOOGLETRANSLATE(INDIRECT("${sourceColLetter}"&ROW())${sep}${langCodeFormula2(`$${sourceColLetter}$1`, sep)}${sep}${langCodeFormula2(`${targetColLetter}$1`, sep)})`;
|
|
1141
1177
|
});
|
|
1142
1178
|
return row;
|
|
1143
1179
|
});
|
|
@@ -1352,6 +1388,367 @@ function mergeSheets(translations, locale, sheetNames) {
|
|
|
1352
1388
|
return merged;
|
|
1353
1389
|
}
|
|
1354
1390
|
|
|
1391
|
+
// src/utils/multiSpreadsheetMerger.ts
|
|
1392
|
+
function mergeMultipleTranslationData(results, mergeStrategy = "later-wins") {
|
|
1393
|
+
const merged = {};
|
|
1394
|
+
for (const result of results) {
|
|
1395
|
+
for (const [locale, sheets] of Object.entries(result)) {
|
|
1396
|
+
if (!merged[locale]) {
|
|
1397
|
+
merged[locale] = {};
|
|
1398
|
+
}
|
|
1399
|
+
for (const [sheet, keys] of Object.entries(sheets)) {
|
|
1400
|
+
if (!merged[locale][sheet]) {
|
|
1401
|
+
merged[locale][sheet] = {};
|
|
1402
|
+
}
|
|
1403
|
+
for (const [key, value] of Object.entries(keys)) {
|
|
1404
|
+
if (mergeStrategy === "first-wins" && key in merged[locale][sheet]) {
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
merged[locale][sheet][key] = value;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return merged;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// src/getMultipleSpreadSheetsData.ts
|
|
1416
|
+
async function getMultipleSpreadSheetsData(docTitles, options = {}) {
|
|
1417
|
+
const { spreadsheetIds, mergeStrategy = "later-wins", ...baseOptions } = options;
|
|
1418
|
+
if (!spreadsheetIds || spreadsheetIds.length === 0) {
|
|
1419
|
+
return getSpreadSheetData(docTitles, baseOptions);
|
|
1420
|
+
}
|
|
1421
|
+
console.log(`[getMultipleSpreadSheetsData] Fetching ${spreadsheetIds.length} spreadsheets...`);
|
|
1422
|
+
const results = [];
|
|
1423
|
+
for (let i = 0; i < spreadsheetIds.length; i++) {
|
|
1424
|
+
const id = spreadsheetIds[i];
|
|
1425
|
+
console.log(`[getMultipleSpreadSheetsData] (${i + 1}/${spreadsheetIds.length}) "${id}"...`);
|
|
1426
|
+
const result = await getSpreadSheetData(docTitles, { ...baseOptions, spreadsheetId: id });
|
|
1427
|
+
results.push(result);
|
|
1428
|
+
}
|
|
1429
|
+
return mergeMultipleTranslationData(results, mergeStrategy);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// src/utils/driveFolderScanner.ts
|
|
1433
|
+
var import_google_auth_library2 = require("google-auth-library");
|
|
1434
|
+
var SPREADSHEET_MIME = "application/vnd.google-apps.spreadsheet";
|
|
1435
|
+
var FOLDER_MIME = "application/vnd.google-apps.folder";
|
|
1436
|
+
var DRIVE_FILES_URL = "https://www.googleapis.com/drive/v3/files";
|
|
1437
|
+
async function getAccessToken(credentials) {
|
|
1438
|
+
const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
|
|
1439
|
+
const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
|
|
1440
|
+
if (!clientEmail || !privateKey) {
|
|
1441
|
+
throw new Error(
|
|
1442
|
+
"Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
const normalizedKey = privateKey.replace(/\\n/g, "\n");
|
|
1446
|
+
const auth = new import_google_auth_library2.GoogleAuth({
|
|
1447
|
+
credentials: { client_email: clientEmail, private_key: normalizedKey },
|
|
1448
|
+
scopes: ["https://www.googleapis.com/auth/drive.readonly"]
|
|
1449
|
+
});
|
|
1450
|
+
const client = await auth.getClient();
|
|
1451
|
+
const tokenResponse = await client.getAccessToken();
|
|
1452
|
+
return tokenResponse.token;
|
|
1453
|
+
}
|
|
1454
|
+
async function listFilesInFolder(folderId, mimeType, token) {
|
|
1455
|
+
const results = [];
|
|
1456
|
+
let pageToken;
|
|
1457
|
+
do {
|
|
1458
|
+
const query = `'${folderId}' in parents and mimeType = '${mimeType}' and trashed = false`;
|
|
1459
|
+
const params = new URLSearchParams({
|
|
1460
|
+
q: query,
|
|
1461
|
+
fields: "nextPageToken,files(id,name,mimeType,modifiedTime,parents)",
|
|
1462
|
+
pageSize: "1000"
|
|
1463
|
+
});
|
|
1464
|
+
if (pageToken) params.set("pageToken", pageToken);
|
|
1465
|
+
const response = await fetch(`${DRIVE_FILES_URL}?${params.toString()}`, {
|
|
1466
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1467
|
+
});
|
|
1468
|
+
if (!response.ok) {
|
|
1469
|
+
const text = await response.text();
|
|
1470
|
+
throw new Error(
|
|
1471
|
+
`Drive API error ${response.status}: ${text}`
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
const data = await response.json();
|
|
1475
|
+
results.push(...data.files);
|
|
1476
|
+
pageToken = data.nextPageToken;
|
|
1477
|
+
} while (pageToken);
|
|
1478
|
+
return results;
|
|
1479
|
+
}
|
|
1480
|
+
async function scanFolder(folderId, folderPath, token, recursive, nameFilter, seen = /* @__PURE__ */ new Set()) {
|
|
1481
|
+
console.log(`[driveFolderScanner] Scanning folder: ${folderId} (path: "${folderPath}")`);
|
|
1482
|
+
const spreadsheets = await listFilesInFolder(folderId, SPREADSHEET_MIME, token);
|
|
1483
|
+
const results = [];
|
|
1484
|
+
for (const file of spreadsheets) {
|
|
1485
|
+
if (seen.has(file.id)) continue;
|
|
1486
|
+
seen.add(file.id);
|
|
1487
|
+
if (nameFilter && !nameFilter.test(file.name)) continue;
|
|
1488
|
+
results.push({
|
|
1489
|
+
id: file.id,
|
|
1490
|
+
name: file.name,
|
|
1491
|
+
folderPath,
|
|
1492
|
+
mimeType: file.mimeType,
|
|
1493
|
+
modifiedTime: file.modifiedTime
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
if (recursive) {
|
|
1497
|
+
const subfolders = await listFilesInFolder(folderId, FOLDER_MIME, token);
|
|
1498
|
+
for (const folder of subfolders) {
|
|
1499
|
+
const subPath = folderPath ? `${folderPath}/${folder.name}` : folder.name;
|
|
1500
|
+
const subResults = await scanFolder(
|
|
1501
|
+
folder.id,
|
|
1502
|
+
subPath,
|
|
1503
|
+
token,
|
|
1504
|
+
recursive,
|
|
1505
|
+
nameFilter,
|
|
1506
|
+
seen
|
|
1507
|
+
);
|
|
1508
|
+
results.push(...subResults);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return results;
|
|
1512
|
+
}
|
|
1513
|
+
async function scanDriveFolderForSpreadsheets(options) {
|
|
1514
|
+
const { folderId, recursive = true, nameFilter, credentials } = options;
|
|
1515
|
+
const token = await getAccessToken(credentials);
|
|
1516
|
+
return scanFolder(folderId, "", token, recursive, nameFilter);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// src/utils/driveImageSync.ts
|
|
1520
|
+
var import_node_fs6 = require("node:fs");
|
|
1521
|
+
var import_node_path5 = require("node:path");
|
|
1522
|
+
var import_promises4 = require("node:stream/promises");
|
|
1523
|
+
var import_node_stream = require("node:stream");
|
|
1524
|
+
var import_google_auth_library3 = require("google-auth-library");
|
|
1525
|
+
var DEFAULT_IMAGE_MIME_TYPES = [
|
|
1526
|
+
"image/jpeg",
|
|
1527
|
+
"image/jpg",
|
|
1528
|
+
"image/png",
|
|
1529
|
+
"image/webp",
|
|
1530
|
+
"image/avif",
|
|
1531
|
+
"image/gif",
|
|
1532
|
+
"image/svg+xml",
|
|
1533
|
+
"image/tiff",
|
|
1534
|
+
"image/bmp",
|
|
1535
|
+
"image/ico",
|
|
1536
|
+
"image/x-icon"
|
|
1537
|
+
];
|
|
1538
|
+
var FOLDER_MIME2 = "application/vnd.google-apps.folder";
|
|
1539
|
+
var DRIVE_FILES_URL2 = "https://www.googleapis.com/drive/v3/files";
|
|
1540
|
+
async function getAccessToken2(credentials) {
|
|
1541
|
+
const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
|
|
1542
|
+
const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
|
|
1543
|
+
if (!clientEmail || !privateKey) {
|
|
1544
|
+
throw new Error(
|
|
1545
|
+
"Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
const normalizedKey = privateKey.replace(/\\n/g, "\n");
|
|
1549
|
+
const auth = new import_google_auth_library3.GoogleAuth({
|
|
1550
|
+
credentials: { client_email: clientEmail, private_key: normalizedKey },
|
|
1551
|
+
scopes: ["https://www.googleapis.com/auth/drive.readonly"]
|
|
1552
|
+
});
|
|
1553
|
+
const client = await auth.getClient();
|
|
1554
|
+
const tokenResponse = await client.getAccessToken();
|
|
1555
|
+
return tokenResponse.token;
|
|
1556
|
+
}
|
|
1557
|
+
async function listFilesInFolder2(folderId, token, mimeTypeFilter) {
|
|
1558
|
+
const results = [];
|
|
1559
|
+
let pageToken;
|
|
1560
|
+
do {
|
|
1561
|
+
const mimeClause = mimeTypeFilter ? ` and mimeType = '${mimeTypeFilter}'` : "";
|
|
1562
|
+
const query = `'${folderId}' in parents${mimeClause} and trashed = false`;
|
|
1563
|
+
const params = new URLSearchParams({
|
|
1564
|
+
q: query,
|
|
1565
|
+
fields: "nextPageToken,files(id,name,mimeType,parents)",
|
|
1566
|
+
pageSize: "1000"
|
|
1567
|
+
});
|
|
1568
|
+
if (pageToken) params.set("pageToken", pageToken);
|
|
1569
|
+
const response = await fetch(`${DRIVE_FILES_URL2}?${params.toString()}`, {
|
|
1570
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1571
|
+
});
|
|
1572
|
+
if (!response.ok) {
|
|
1573
|
+
const text = await response.text();
|
|
1574
|
+
throw new Error(`Drive API error ${response.status}: ${text}`);
|
|
1575
|
+
}
|
|
1576
|
+
const data = await response.json();
|
|
1577
|
+
results.push(...data.files);
|
|
1578
|
+
pageToken = data.nextPageToken;
|
|
1579
|
+
} while (pageToken);
|
|
1580
|
+
return results;
|
|
1581
|
+
}
|
|
1582
|
+
async function collectFiles(folderId, folderRelPath, outputPath, token, allowedMimeTypes, recursive, folderPattern) {
|
|
1583
|
+
console.log(`[driveImageSync] Scanning folder: ${folderId} (path: "${folderRelPath}")`);
|
|
1584
|
+
const allItems = await listFilesInFolder2(folderId, token);
|
|
1585
|
+
const entries = [];
|
|
1586
|
+
for (const item of allItems) {
|
|
1587
|
+
if (item.mimeType === FOLDER_MIME2) {
|
|
1588
|
+
if (!recursive) continue;
|
|
1589
|
+
const subRelPath = folderRelPath ? `${folderRelPath}/${item.name}` : item.name;
|
|
1590
|
+
if (folderPattern && !folderPattern.test(subRelPath)) continue;
|
|
1591
|
+
const subEntries = await collectFiles(
|
|
1592
|
+
item.id,
|
|
1593
|
+
subRelPath,
|
|
1594
|
+
outputPath,
|
|
1595
|
+
token,
|
|
1596
|
+
allowedMimeTypes,
|
|
1597
|
+
recursive,
|
|
1598
|
+
folderPattern
|
|
1599
|
+
);
|
|
1600
|
+
entries.push(...subEntries);
|
|
1601
|
+
} else if (allowedMimeTypes.includes(item.mimeType)) {
|
|
1602
|
+
const localPath = folderRelPath ? (0, import_node_path5.join)(outputPath, folderRelPath, item.name) : (0, import_node_path5.join)(outputPath, item.name);
|
|
1603
|
+
entries.push({ id: item.id, name: item.name, localPath, mimeType: item.mimeType });
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return entries;
|
|
1607
|
+
}
|
|
1608
|
+
async function downloadFile(fileId, localPath, token) {
|
|
1609
|
+
const url = `${DRIVE_FILES_URL2}/${fileId}?alt=media`;
|
|
1610
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
1611
|
+
if (!response.ok) {
|
|
1612
|
+
throw new Error(`Failed to download ${fileId}: ${response.status}`);
|
|
1613
|
+
}
|
|
1614
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path5.dirname)(localPath), { recursive: true });
|
|
1615
|
+
const dest = (0, import_node_fs6.createWriteStream)(localPath);
|
|
1616
|
+
await (0, import_promises4.pipeline)(import_node_stream.Readable.fromWeb(response.body), dest);
|
|
1617
|
+
}
|
|
1618
|
+
function collectLocalFiles(dir, base) {
|
|
1619
|
+
const results = [];
|
|
1620
|
+
if (!(0, import_node_fs6.existsSync)(dir)) return results;
|
|
1621
|
+
for (const entry of (0, import_node_fs6.readdirSync)(dir)) {
|
|
1622
|
+
const fullPath = (0, import_node_path5.join)(dir, entry);
|
|
1623
|
+
const stat = (0, import_node_fs6.statSync)(fullPath);
|
|
1624
|
+
if (stat.isDirectory()) {
|
|
1625
|
+
results.push(...collectLocalFiles(fullPath, base));
|
|
1626
|
+
} else {
|
|
1627
|
+
results.push(fullPath);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return results;
|
|
1631
|
+
}
|
|
1632
|
+
async function runConcurrent(tasks, concurrency) {
|
|
1633
|
+
const results = [];
|
|
1634
|
+
for (let i = 0; i < tasks.length; i += concurrency) {
|
|
1635
|
+
const batch = tasks.slice(i, i + concurrency).map((t) => t());
|
|
1636
|
+
results.push(...await Promise.all(batch));
|
|
1637
|
+
}
|
|
1638
|
+
return results;
|
|
1639
|
+
}
|
|
1640
|
+
async function syncDriveImages(options) {
|
|
1641
|
+
const {
|
|
1642
|
+
folderId,
|
|
1643
|
+
outputPath,
|
|
1644
|
+
mimeTypes = DEFAULT_IMAGE_MIME_TYPES,
|
|
1645
|
+
recursive = true,
|
|
1646
|
+
folderPattern,
|
|
1647
|
+
credentials,
|
|
1648
|
+
cleanSync = false,
|
|
1649
|
+
concurrency = 3
|
|
1650
|
+
} = options;
|
|
1651
|
+
const token = await getAccessToken2(credentials);
|
|
1652
|
+
(0, import_node_fs6.mkdirSync)(outputPath, { recursive: true });
|
|
1653
|
+
const entries = await collectFiles(
|
|
1654
|
+
folderId,
|
|
1655
|
+
"",
|
|
1656
|
+
outputPath,
|
|
1657
|
+
token,
|
|
1658
|
+
mimeTypes,
|
|
1659
|
+
recursive,
|
|
1660
|
+
folderPattern
|
|
1661
|
+
);
|
|
1662
|
+
const downloaded = [];
|
|
1663
|
+
const skipped = [];
|
|
1664
|
+
const errors = [];
|
|
1665
|
+
const tasks = entries.map((entry) => async () => {
|
|
1666
|
+
if ((0, import_node_fs6.existsSync)(entry.localPath)) {
|
|
1667
|
+
console.log(`[driveImageSync] Skipping (exists): ${entry.localPath}`);
|
|
1668
|
+
skipped.push(entry.localPath);
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
console.log(`[driveImageSync] Downloading: ${entry.localPath}`);
|
|
1672
|
+
try {
|
|
1673
|
+
await downloadFile(entry.id, entry.localPath, token);
|
|
1674
|
+
downloaded.push(entry.localPath);
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1677
|
+
console.error(`[driveImageSync] Error downloading ${entry.localPath}: ${msg}`);
|
|
1678
|
+
errors.push(entry.localPath);
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
await runConcurrent(tasks, concurrency);
|
|
1682
|
+
const deleted = [];
|
|
1683
|
+
if (cleanSync) {
|
|
1684
|
+
const driveLocalPaths = new Set(entries.map((e) => e.localPath));
|
|
1685
|
+
const localFiles = collectLocalFiles(outputPath, outputPath);
|
|
1686
|
+
for (const localFile of localFiles) {
|
|
1687
|
+
if (!driveLocalPaths.has(localFile)) {
|
|
1688
|
+
console.log(`[driveImageSync] Deleting (not in Drive): ${localFile}`);
|
|
1689
|
+
(0, import_node_fs6.unlinkSync)(localFile);
|
|
1690
|
+
deleted.push(localFile);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
console.log(
|
|
1695
|
+
`[driveImageSync] Synced ${downloaded.length} files, skipped ${skipped.length}, deleted ${deleted.length}, errors ${errors.length}`
|
|
1696
|
+
);
|
|
1697
|
+
return { downloaded, skipped, deleted, errors };
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/utils/getDriveTranslations.ts
|
|
1701
|
+
async function manageDriveTranslations(options) {
|
|
1702
|
+
const {
|
|
1703
|
+
driveFolderId,
|
|
1704
|
+
scanForSpreadsheets = true,
|
|
1705
|
+
spreadsheetIds: explicitIds = [],
|
|
1706
|
+
spreadsheetNameFilter,
|
|
1707
|
+
syncImages = false,
|
|
1708
|
+
imageOutputPath,
|
|
1709
|
+
imageSyncOptions,
|
|
1710
|
+
translationOptions = {},
|
|
1711
|
+
docTitles
|
|
1712
|
+
} = options;
|
|
1713
|
+
if (syncImages && !imageOutputPath) {
|
|
1714
|
+
throw new Error(
|
|
1715
|
+
"[manageDriveTranslations] imageOutputPath is required when syncImages is true"
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
const discoveredIds = [];
|
|
1719
|
+
const discoveredNames = /* @__PURE__ */ new Map();
|
|
1720
|
+
if (driveFolderId && scanForSpreadsheets) {
|
|
1721
|
+
const scanOptions = { folderId: driveFolderId };
|
|
1722
|
+
const discovered = await scanDriveFolderForSpreadsheets(scanOptions);
|
|
1723
|
+
console.log(
|
|
1724
|
+
`[manageDriveTranslations] Found ${discovered.length} spreadsheet(s) in Drive folder`
|
|
1725
|
+
);
|
|
1726
|
+
for (const file of discovered) {
|
|
1727
|
+
discoveredIds.push(file.id);
|
|
1728
|
+
discoveredNames.set(file.id, file.name);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
const allIds = [.../* @__PURE__ */ new Set([...discoveredIds, ...explicitIds])];
|
|
1732
|
+
const filteredIds = spreadsheetNameFilter ? allIds.filter((id) => {
|
|
1733
|
+
const name = discoveredNames.get(id);
|
|
1734
|
+
if (!name) return true;
|
|
1735
|
+
return spreadsheetNameFilter.test(name);
|
|
1736
|
+
}) : allIds;
|
|
1737
|
+
const translations = await getMultipleSpreadSheetsData(docTitles, {
|
|
1738
|
+
...translationOptions,
|
|
1739
|
+
spreadsheetIds: filteredIds.length > 0 ? filteredIds : void 0
|
|
1740
|
+
});
|
|
1741
|
+
let imageSync;
|
|
1742
|
+
if (syncImages && driveFolderId && imageOutputPath) {
|
|
1743
|
+
imageSync = await syncDriveImages({
|
|
1744
|
+
...imageSyncOptions,
|
|
1745
|
+
folderId: driveFolderId,
|
|
1746
|
+
outputPath: imageOutputPath
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
return { translations, spreadsheetIds: filteredIds, imageSync };
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1355
1752
|
// src/index.ts
|
|
1356
1753
|
var index_default = getSpreadSheetData;
|
|
1357
1754
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -1364,19 +1761,25 @@ var index_default = getSpreadSheetData;
|
|
|
1364
1761
|
createSpreadsheet,
|
|
1365
1762
|
filterValidLocales,
|
|
1366
1763
|
findLocalChanges,
|
|
1764
|
+
getGoogleTranslateCode,
|
|
1367
1765
|
getLanguagePrefix,
|
|
1368
1766
|
getLocaleDisplayName,
|
|
1767
|
+
getMultipleSpreadSheetsData,
|
|
1369
1768
|
getNormalizedLocaleForHeader,
|
|
1370
1769
|
getOriginalHeaderForLocale,
|
|
1371
1770
|
getSpreadSheetData,
|
|
1372
1771
|
getTranslationSummary,
|
|
1373
1772
|
handleBidirectionalSync,
|
|
1374
1773
|
isValidLocale,
|
|
1774
|
+
manageDriveTranslations,
|
|
1775
|
+
mergeMultipleTranslationData,
|
|
1375
1776
|
mergeSheets,
|
|
1376
1777
|
normalizeLocaleCode,
|
|
1377
1778
|
processRawRows,
|
|
1378
1779
|
readPublicSheet,
|
|
1379
1780
|
resolveLocaleWithFallback,
|
|
1781
|
+
scanDriveFolderForSpreadsheets,
|
|
1782
|
+
syncDriveImages,
|
|
1380
1783
|
updateSpreadsheetWithLocalChanges,
|
|
1381
1784
|
validateCredentials,
|
|
1382
1785
|
validateEnv,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { GoogleEnvVars } from '../types';
|
|
2
|
+
export interface DriveSpreadsheetFile {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
/** Relative path within the Drive folder (e.g., "subproject/translations") */
|
|
6
|
+
folderPath: string;
|
|
7
|
+
mimeType: string;
|
|
8
|
+
modifiedTime?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ScanDriveFolderOptions {
|
|
11
|
+
/** Google Drive folder ID to scan */
|
|
12
|
+
folderId: string;
|
|
13
|
+
/** Whether to recursively scan subfolders (default: true) */
|
|
14
|
+
recursive?: boolean;
|
|
15
|
+
/** Only return spreadsheets with names matching this pattern (optional) */
|
|
16
|
+
nameFilter?: RegExp;
|
|
17
|
+
/** Google service account credentials (falls back to env vars) */
|
|
18
|
+
credentials?: GoogleEnvVars;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Scans a Google Drive folder for Google Spreadsheet files.
|
|
22
|
+
* Returns all spreadsheets found (recursively by default).
|
|
23
|
+
* Uses Drive API v3 via authenticated fetch (no googleapis package needed).
|
|
24
|
+
*/
|
|
25
|
+
export declare function scanDriveFolderForSpreadsheets(options: ScanDriveFolderOptions): Promise<DriveSpreadsheetFile[]>;
|
|
26
|
+
//# sourceMappingURL=driveFolderScanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driveFolderScanner.d.ts","sourceRoot":"","sources":["../../src/utils/driveFolderScanner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,WAAW,CAAC,EAAE,aAAa,CAAC;CAC7B;AA8HD;;;;GAIG;AACH,wBAAsB,8BAA8B,CAClD,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAKjC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { GoogleEnvVars } from '../types';
|
|
2
|
+
export interface DriveImageSyncOptions {
|
|
3
|
+
/** Google Drive root folder ID to sync images from */
|
|
4
|
+
folderId: string;
|
|
5
|
+
/** Local directory to download images into (will be created if missing) */
|
|
6
|
+
outputPath: string;
|
|
7
|
+
/** Only download files matching these MIME types (default: all image types) */
|
|
8
|
+
mimeTypes?: string[];
|
|
9
|
+
/** Whether to recursively sync subfolders (default: true) */
|
|
10
|
+
recursive?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Subfolder filter pattern. If provided, only subfolders whose relative path
|
|
13
|
+
* (from the root folderId) matches this pattern will be synced.
|
|
14
|
+
* The pattern is tested against the full relative path, e.g. "projects/icons".
|
|
15
|
+
* Useful for patterns like `/^projects\//` or `/icons$/`.
|
|
16
|
+
*/
|
|
17
|
+
folderPattern?: RegExp;
|
|
18
|
+
/** Google service account credentials (falls back to env vars) */
|
|
19
|
+
credentials?: GoogleEnvVars;
|
|
20
|
+
/** If true, delete local files that no longer exist in Drive (default: false) */
|
|
21
|
+
cleanSync?: boolean;
|
|
22
|
+
/** Max concurrent downloads (default: 3) */
|
|
23
|
+
concurrency?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface DriveImageSyncResult {
|
|
26
|
+
downloaded: string[];
|
|
27
|
+
skipped: string[];
|
|
28
|
+
deleted: string[];
|
|
29
|
+
errors: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Syncs images from a Google Drive folder to a local directory.
|
|
33
|
+
* Preserves subfolder structure. Uses Drive API v3 via fetch.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* await syncDriveImages({
|
|
37
|
+
* folderId: 'your-drive-folder-id',
|
|
38
|
+
* outputPath: './src/assets/remote-images',
|
|
39
|
+
* recursive: true,
|
|
40
|
+
* credentials: { GOOGLE_CLIENT_EMAIL: '...', GOOGLE_PRIVATE_KEY: '...', GOOGLE_SPREADSHEET_ID: '' }
|
|
41
|
+
* });
|
|
42
|
+
*/
|
|
43
|
+
export declare function syncDriveImages(options: DriveImageSyncOptions): Promise<DriveImageSyncResult>;
|
|
44
|
+
//# sourceMappingURL=driveImageSync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driveImageSync.d.ts","sourceRoot":"","sources":["../../src/utils/driveImageSync.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,MAAM,WAAW,qBAAqB;IACpC,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B,iFAAiF;IACjF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAoLD;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAoE/B"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { TranslationData } from '../types';
|
|
2
|
+
import type { MultiSpreadsheetOptions } from '../getMultipleSpreadSheetsData';
|
|
3
|
+
import type { DriveImageSyncOptions, DriveImageSyncResult } from './driveImageSync';
|
|
4
|
+
export interface GoogleDriveManagerOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Google Drive folder ID to scan for spreadsheets and/or images.
|
|
7
|
+
* If provided without explicit spreadsheetIds, the folder is scanned
|
|
8
|
+
* automatically for spreadsheet files.
|
|
9
|
+
*/
|
|
10
|
+
driveFolderId?: string;
|
|
11
|
+
/**
|
|
12
|
+
* When true, scans driveFolderId for all Google Spreadsheet files and
|
|
13
|
+
* fetches translations from each. Requires driveFolderId. (default: true when driveFolderId set)
|
|
14
|
+
*/
|
|
15
|
+
scanForSpreadsheets?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Explicit list of spreadsheet IDs to fetch from.
|
|
18
|
+
* If provided together with driveFolderId + scanForSpreadsheets, the
|
|
19
|
+
* explicit list is merged with the discovered ones (deduped).
|
|
20
|
+
*/
|
|
21
|
+
spreadsheetIds?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Optional filter: only process spreadsheets whose name matches this pattern.
|
|
24
|
+
* Useful when the Drive folder contains non-translation spreadsheets.
|
|
25
|
+
* @example /^translations-/i
|
|
26
|
+
*/
|
|
27
|
+
spreadsheetNameFilter?: RegExp;
|
|
28
|
+
/**
|
|
29
|
+
* When true, also sync images from driveFolderId to imageOutputPath.
|
|
30
|
+
* Requires driveFolderId. (default: false)
|
|
31
|
+
*/
|
|
32
|
+
syncImages?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Local directory to download Drive images into.
|
|
35
|
+
* Required when syncImages: true.
|
|
36
|
+
* @example './src/assets/remote-images'
|
|
37
|
+
*/
|
|
38
|
+
imageOutputPath?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Image sync options passed to syncDriveImages (mimeTypes, concurrency, etc.)
|
|
41
|
+
*/
|
|
42
|
+
imageSyncOptions?: Partial<DriveImageSyncOptions>;
|
|
43
|
+
/**
|
|
44
|
+
* Options forwarded to getMultipleSpreadSheetsData (rowLimit, waitSeconds,
|
|
45
|
+
* translationsOutputDir, autoTranslate, etc.)
|
|
46
|
+
*/
|
|
47
|
+
translationOptions?: MultiSpreadsheetOptions;
|
|
48
|
+
/** Sheet names to fetch from each discovered spreadsheet */
|
|
49
|
+
docTitles?: string[];
|
|
50
|
+
}
|
|
51
|
+
export interface GoogleDriveManagerResult {
|
|
52
|
+
translations: TranslationData;
|
|
53
|
+
/** List of spreadsheet IDs that were processed */
|
|
54
|
+
spreadsheetIds: string[];
|
|
55
|
+
/** Image sync result (only present if syncImages: true) */
|
|
56
|
+
imageSync?: DriveImageSyncResult;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Top-level "headless CMS bridge" function.
|
|
60
|
+
*
|
|
61
|
+
* Scans a Google Drive folder for spreadsheets, fetches all translations,
|
|
62
|
+
* optionally syncs images, and returns merged results.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const result = await manageDriveTranslations({
|
|
66
|
+
* driveFolderId: 'your-folder-id',
|
|
67
|
+
* scanForSpreadsheets: true,
|
|
68
|
+
* spreadsheetNameFilter: /^i18n-/,
|
|
69
|
+
* syncImages: true,
|
|
70
|
+
* imageOutputPath: './src/assets/remote-images',
|
|
71
|
+
* translationOptions: {
|
|
72
|
+
* autoTranslate: false,
|
|
73
|
+
* translationsOutputDir: './src/translations'
|
|
74
|
+
* }
|
|
75
|
+
* });
|
|
76
|
+
* console.log(result.translations);
|
|
77
|
+
* console.log(result.imageSync?.downloaded.length + ' images downloaded');
|
|
78
|
+
*/
|
|
79
|
+
export declare function manageDriveTranslations(options: GoogleDriveManagerOptions): Promise<GoogleDriveManagerResult>;
|
|
80
|
+
//# sourceMappingURL=getDriveTranslations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getDriveTranslations.d.ts","sourceRoot":"","sources":["../../src/utils/getDriveTranslations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAI9E,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAGpF,MAAM,WAAW,yBAAyB;IACzC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAE1B;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAElD;;;OAGG;IACH,kBAAkB,CAAC,EAAE,uBAAuB,CAAC;IAE7C,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACxC,YAAY,EAAE,eAAe,CAAC;IAC9B,kDAAkD;IAClD,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,oBAAoB,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,uBAAuB,CAC5C,OAAO,EAAE,yBAAyB,GAChC,OAAO,CAAC,wBAAwB,CAAC,CAkEnC"}
|