@el-j/google-sheet-translations 2.2.0-beta.3 → 2.2.0-beta.4
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/dist/action-entrypoint.d.ts.map +1 -1
- package/dist/esm/index.js +474 -12
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +483 -12
- package/dist/utils/docIngester.d.ts +111 -0
- package/dist/utils/docIngester.d.ts.map +1 -0
- package/dist/utils/docParser.d.ts +77 -0
- package/dist/utils/docParser.d.ts.map +1 -0
- package/dist/utils/driveDocScanner.d.ts +58 -0
- package/dist/utils/driveDocScanner.d.ts.map +1 -0
- package/dist/utils/driveProjectIndex.d.ts +39 -0
- package/dist/utils/driveProjectIndex.d.ts.map +1 -1
- package/dist/utils/getDriveTranslations.d.ts +46 -0
- package/dist/utils/getDriveTranslations.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -40,6 +40,9 @@ __export(index_exports, {
|
|
|
40
40
|
createLocaleMapping: () => createLocaleMapping,
|
|
41
41
|
createSpreadsheet: () => createSpreadsheet,
|
|
42
42
|
default: () => index_default,
|
|
43
|
+
entriesToSeedKeys: () => entriesToSeedKeys,
|
|
44
|
+
entriesToTranslationData: () => entriesToTranslationData,
|
|
45
|
+
exportDoc: () => exportDoc,
|
|
43
46
|
filterValidLocales: () => filterValidLocales,
|
|
44
47
|
findLocalChanges: () => findLocalChanges,
|
|
45
48
|
getGoogleTranslateCode: () => getGoogleTranslateCode,
|
|
@@ -51,6 +54,8 @@ __export(index_exports, {
|
|
|
51
54
|
getSpreadSheetData: () => getSpreadSheetData,
|
|
52
55
|
getTranslationSummary: () => getTranslationSummary,
|
|
53
56
|
handleBidirectionalSync: () => handleBidirectionalSync,
|
|
57
|
+
inferLocaleFromDocName: () => inferLocaleFromDocName,
|
|
58
|
+
ingestDoc: () => ingestDoc,
|
|
54
59
|
isValidLocale: () => isValidLocale,
|
|
55
60
|
manageDriveTranslations: () => manageDriveTranslations,
|
|
56
61
|
mergeMultipleTranslationData: () => mergeMultipleTranslationData,
|
|
@@ -58,10 +63,14 @@ __export(index_exports, {
|
|
|
58
63
|
normalizeExtension: () => normalizeExtension,
|
|
59
64
|
normalizeLocaleCode: () => normalizeLocaleCode,
|
|
60
65
|
normalizePrivateKey: () => normalizePrivateKey,
|
|
66
|
+
parseDocContent: () => parseDocContent,
|
|
61
67
|
processRawRows: () => processRawRows,
|
|
68
|
+
readManifest: () => readManifest,
|
|
62
69
|
readPublicSheet: () => readPublicSheet,
|
|
63
70
|
resolveLocaleWithFallback: () => resolveLocaleWithFallback,
|
|
71
|
+
scanDriveFolderForDocs: () => scanDriveFolderForDocs,
|
|
64
72
|
scanDriveFolderForSpreadsheets: () => scanDriveFolderForSpreadsheets,
|
|
73
|
+
slugifyKey: () => slugifyKey,
|
|
65
74
|
syncDriveImages: () => syncDriveImages,
|
|
66
75
|
updateSpreadsheetWithLocalChanges: () => updateSpreadsheetWithLocalChanges,
|
|
67
76
|
validateCredentials: () => validateCredentials,
|
|
@@ -1267,13 +1276,13 @@ GOOGLE_SPREADSHEET_ID=${id}
|
|
|
1267
1276
|
async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
|
|
1268
1277
|
const config = normalizeConfig(options);
|
|
1269
1278
|
const baseDelayMs = config.waitSeconds * 1e3;
|
|
1270
|
-
const
|
|
1271
|
-
if (
|
|
1279
|
+
const docTitle2 = _docTitle ?? [];
|
|
1280
|
+
if (docTitle2.length === 0) {
|
|
1272
1281
|
console.warn("No sheet titles provided, cannot process spreadsheet data");
|
|
1273
1282
|
return {};
|
|
1274
1283
|
}
|
|
1275
|
-
if (!
|
|
1276
|
-
|
|
1284
|
+
if (!docTitle2.includes("i18n")) {
|
|
1285
|
+
docTitle2.push("i18n");
|
|
1277
1286
|
}
|
|
1278
1287
|
const finalTranslations = {};
|
|
1279
1288
|
const allLocales = /* @__PURE__ */ new Set();
|
|
@@ -1310,9 +1319,9 @@ async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
|
|
|
1310
1319
|
"No spreadsheet ID provided. Set GOOGLE_SPREADSHEET_ID or pass spreadsheetId in options."
|
|
1311
1320
|
);
|
|
1312
1321
|
}
|
|
1313
|
-
console.log(`Processing ${
|
|
1322
|
+
console.log(`Processing ${docTitle2.length} sheets: ${docTitle2.join(", ")}`);
|
|
1314
1323
|
await Promise.all(
|
|
1315
|
-
|
|
1324
|
+
docTitle2.map(async (title) => {
|
|
1316
1325
|
let rows;
|
|
1317
1326
|
try {
|
|
1318
1327
|
rows = await withRetry(
|
|
@@ -1345,7 +1354,7 @@ async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
|
|
|
1345
1354
|
);
|
|
1346
1355
|
}
|
|
1347
1356
|
}
|
|
1348
|
-
console.log(`Processing ${
|
|
1357
|
+
console.log(`Processing ${docTitle2.length} sheets: ${docTitle2.join(", ")}`);
|
|
1349
1358
|
const doc = new import_google_spreadsheet2.GoogleSpreadsheet(spreadsheetId, serviceAuthClient);
|
|
1350
1359
|
try {
|
|
1351
1360
|
await withRetry(() => doc.loadInfo(true), "loadInfo", baseDelayMs);
|
|
@@ -1353,7 +1362,7 @@ async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
|
|
|
1353
1362
|
throw new Error(`Failed to load spreadsheet "${spreadsheetId}"`, { cause: err });
|
|
1354
1363
|
}
|
|
1355
1364
|
await Promise.all(
|
|
1356
|
-
|
|
1365
|
+
docTitle2.map(async (title) => {
|
|
1357
1366
|
const sheet = doc.sheetsByTitle[title];
|
|
1358
1367
|
if (!sheet) {
|
|
1359
1368
|
console.warn(`Sheet "${title}" not found in the document`);
|
|
@@ -1888,6 +1897,7 @@ function buildManifest(options) {
|
|
|
1888
1897
|
locales,
|
|
1889
1898
|
defaultLocale: options.defaultLocale,
|
|
1890
1899
|
spreadsheets: options.spreadsheets,
|
|
1900
|
+
docs: options.docs,
|
|
1891
1901
|
outputDirectory: options.outputDirectory,
|
|
1892
1902
|
flatten: options.flatten,
|
|
1893
1903
|
projectMetadata: options.projectMetadata
|
|
@@ -1901,11 +1911,385 @@ function writeManifest(manifest, manifestPath) {
|
|
|
1901
1911
|
import_node_fs8.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
1902
1912
|
console.log(`[driveProjectIndex] Wrote project manifest \u2192 ${manifestPath}`);
|
|
1903
1913
|
}
|
|
1914
|
+
function readManifest(manifestPath) {
|
|
1915
|
+
try {
|
|
1916
|
+
const content = import_node_fs8.default.readFileSync(manifestPath, "utf8");
|
|
1917
|
+
return JSON.parse(content);
|
|
1918
|
+
} catch {
|
|
1919
|
+
return void 0;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// src/utils/driveDocScanner.ts
|
|
1924
|
+
var import_google_auth_library2 = require("google-auth-library");
|
|
1925
|
+
var DOC_MIME = "application/vnd.google-apps.document";
|
|
1926
|
+
var FOLDER_MIME3 = "application/vnd.google-apps.folder";
|
|
1927
|
+
var DRIVE_FILES_URL3 = "https://www.googleapis.com/drive/v3/files";
|
|
1928
|
+
async function getAccessToken3(credentials) {
|
|
1929
|
+
const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
|
|
1930
|
+
const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
|
|
1931
|
+
if (!clientEmail || !privateKey) {
|
|
1932
|
+
throw new Error(
|
|
1933
|
+
"Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
const normalizedKey = privateKey.replace(/\\n/g, "\n");
|
|
1937
|
+
const auth = new import_google_auth_library2.GoogleAuth({
|
|
1938
|
+
credentials: { client_email: clientEmail, private_key: normalizedKey },
|
|
1939
|
+
scopes: ["https://www.googleapis.com/auth/drive.readonly"]
|
|
1940
|
+
});
|
|
1941
|
+
const client = await auth.getClient();
|
|
1942
|
+
const tokenResponse = await client.getAccessToken();
|
|
1943
|
+
return tokenResponse.token;
|
|
1944
|
+
}
|
|
1945
|
+
async function listFilesInFolder3(folderId, mimeType, token) {
|
|
1946
|
+
const results = [];
|
|
1947
|
+
let pageToken;
|
|
1948
|
+
do {
|
|
1949
|
+
const query = `'${folderId}' in parents and mimeType = '${mimeType}' and trashed = false`;
|
|
1950
|
+
const params = new URLSearchParams({
|
|
1951
|
+
q: query,
|
|
1952
|
+
fields: "nextPageToken,files(id,name,mimeType,modifiedTime)",
|
|
1953
|
+
pageSize: "1000"
|
|
1954
|
+
});
|
|
1955
|
+
if (pageToken) params.set("pageToken", pageToken);
|
|
1956
|
+
const response = await fetch(`${DRIVE_FILES_URL3}?${params.toString()}`, {
|
|
1957
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1958
|
+
});
|
|
1959
|
+
if (!response.ok) {
|
|
1960
|
+
const text = await response.text();
|
|
1961
|
+
throw new Error(`Drive API error ${response.status}: ${text}`);
|
|
1962
|
+
}
|
|
1963
|
+
const data = await response.json();
|
|
1964
|
+
results.push(...data.files);
|
|
1965
|
+
pageToken = data.nextPageToken;
|
|
1966
|
+
} while (pageToken);
|
|
1967
|
+
return results;
|
|
1968
|
+
}
|
|
1969
|
+
function inferLocaleFromDocName(name) {
|
|
1970
|
+
const baseName = name.replace(/\.[^.]+$/, "");
|
|
1971
|
+
const match = baseName.match(/_([a-zA-Z]{2,3}(?:[-_][a-zA-Z]{2,4})?)$/);
|
|
1972
|
+
if (!match) return void 0;
|
|
1973
|
+
const candidate = match[1].replace("_", "-");
|
|
1974
|
+
const parts = candidate.split("-");
|
|
1975
|
+
if (parts.length === 2) {
|
|
1976
|
+
return `${parts[0].toLowerCase()}-${parts[1].toUpperCase()}`;
|
|
1977
|
+
}
|
|
1978
|
+
return parts[0].toLowerCase();
|
|
1979
|
+
}
|
|
1980
|
+
async function scanFolder2(folderId, folderPath, token, recursive, nameFilter, seen = /* @__PURE__ */ new Set()) {
|
|
1981
|
+
console.log(
|
|
1982
|
+
`[driveDocScanner] Scanning folder: ${folderId} (path: "${folderPath}")`
|
|
1983
|
+
);
|
|
1984
|
+
const docs = await listFilesInFolder3(folderId, DOC_MIME, token);
|
|
1985
|
+
const results = [];
|
|
1986
|
+
for (const file of docs) {
|
|
1987
|
+
if (seen.has(file.id)) continue;
|
|
1988
|
+
seen.add(file.id);
|
|
1989
|
+
if (nameFilter && !nameFilter.test(file.name)) continue;
|
|
1990
|
+
results.push({
|
|
1991
|
+
id: file.id,
|
|
1992
|
+
name: file.name,
|
|
1993
|
+
folderPath,
|
|
1994
|
+
mimeType: file.mimeType,
|
|
1995
|
+
modifiedTime: file.modifiedTime,
|
|
1996
|
+
sourceLocale: inferLocaleFromDocName(file.name)
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
if (recursive) {
|
|
2000
|
+
const subfolders = await listFilesInFolder3(folderId, FOLDER_MIME3, token);
|
|
2001
|
+
for (const folder of subfolders) {
|
|
2002
|
+
const subPath = folderPath ? `${folderPath}/${folder.name}` : folder.name;
|
|
2003
|
+
const subResults = await scanFolder2(
|
|
2004
|
+
folder.id,
|
|
2005
|
+
subPath,
|
|
2006
|
+
token,
|
|
2007
|
+
recursive,
|
|
2008
|
+
nameFilter,
|
|
2009
|
+
seen
|
|
2010
|
+
);
|
|
2011
|
+
results.push(...subResults);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
return results;
|
|
2015
|
+
}
|
|
2016
|
+
async function scanDriveFolderForDocs(options) {
|
|
2017
|
+
const { folderId, recursive = true, nameFilter, credentials } = options;
|
|
2018
|
+
const token = await getAccessToken3(credentials);
|
|
2019
|
+
return scanFolder2(folderId, "", token, recursive, nameFilter);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// src/utils/docIngester.ts
|
|
2023
|
+
var import_google_auth_library3 = require("google-auth-library");
|
|
2024
|
+
var import_google_spreadsheet3 = require("google-spreadsheet");
|
|
2025
|
+
|
|
2026
|
+
// src/utils/docParser.ts
|
|
2027
|
+
function slugifyKey(text) {
|
|
2028
|
+
return text.toLowerCase().replace(/[^\w]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
2029
|
+
}
|
|
2030
|
+
function parseDocContent(content, options = {}) {
|
|
2031
|
+
const { strategy = "heading", defaultSheetName = "content" } = options;
|
|
2032
|
+
if (strategy === "marker") return parseWithMarkers(content, defaultSheetName);
|
|
2033
|
+
if (strategy === "numbered") return parseNumbered(content, defaultSheetName);
|
|
2034
|
+
return parseWithHeadings(content, defaultSheetName);
|
|
2035
|
+
}
|
|
2036
|
+
function parseWithHeadings(content, defaultSheetName) {
|
|
2037
|
+
const lines = content.split("\n");
|
|
2038
|
+
const entries = [];
|
|
2039
|
+
let currentSheet = defaultSheetName;
|
|
2040
|
+
let currentKey = null;
|
|
2041
|
+
const valueLines = [];
|
|
2042
|
+
function flushEntry() {
|
|
2043
|
+
if (currentKey !== null) {
|
|
2044
|
+
const value = valueLines.join("\n").trim();
|
|
2045
|
+
if (value) {
|
|
2046
|
+
entries.push({ sheetName: currentSheet, key: currentKey, value });
|
|
2047
|
+
}
|
|
2048
|
+
currentKey = null;
|
|
2049
|
+
valueLines.length = 0;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
for (const rawLine of lines) {
|
|
2053
|
+
const line = rawLine.trimEnd();
|
|
2054
|
+
if (line.startsWith("# ")) {
|
|
2055
|
+
flushEntry();
|
|
2056
|
+
currentSheet = slugifyKey(line.slice(2).trim()) || defaultSheetName;
|
|
2057
|
+
currentKey = null;
|
|
2058
|
+
valueLines.length = 0;
|
|
2059
|
+
} else if (line.startsWith("## ")) {
|
|
2060
|
+
flushEntry();
|
|
2061
|
+
currentKey = slugifyKey(line.slice(3).trim());
|
|
2062
|
+
valueLines.length = 0;
|
|
2063
|
+
} else if (currentKey !== null) {
|
|
2064
|
+
valueLines.push(line);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
flushEntry();
|
|
2068
|
+
return entries;
|
|
2069
|
+
}
|
|
2070
|
+
function parseWithMarkers(content, defaultSheetName) {
|
|
2071
|
+
const MARKER_RE = /\[\[key:([^\]]{1,200})\]\]/g;
|
|
2072
|
+
const entries = [];
|
|
2073
|
+
const segments = content.split(MARKER_RE);
|
|
2074
|
+
for (let i = 1; i < segments.length; i += 2) {
|
|
2075
|
+
const keyPath = segments[i].trim();
|
|
2076
|
+
const value = (segments[i + 1] ?? "").trim();
|
|
2077
|
+
if (!keyPath || !value) continue;
|
|
2078
|
+
const dotIdx = keyPath.indexOf(".");
|
|
2079
|
+
let sheetName;
|
|
2080
|
+
let key;
|
|
2081
|
+
if (dotIdx !== -1) {
|
|
2082
|
+
sheetName = slugifyKey(keyPath.slice(0, dotIdx));
|
|
2083
|
+
key = slugifyKey(keyPath.slice(dotIdx + 1));
|
|
2084
|
+
} else {
|
|
2085
|
+
sheetName = defaultSheetName;
|
|
2086
|
+
key = slugifyKey(keyPath);
|
|
2087
|
+
}
|
|
2088
|
+
if (sheetName && key) {
|
|
2089
|
+
entries.push({ sheetName, key, value });
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return entries;
|
|
2093
|
+
}
|
|
2094
|
+
function parseNumbered(content, defaultSheetName) {
|
|
2095
|
+
const entries = [];
|
|
2096
|
+
let counter = 0;
|
|
2097
|
+
const paragraphs = content.split(/\n{2,}/);
|
|
2098
|
+
for (const para of paragraphs) {
|
|
2099
|
+
const value = para.replace(/^[#\s]+/, "").trim();
|
|
2100
|
+
if (value) {
|
|
2101
|
+
counter++;
|
|
2102
|
+
entries.push({
|
|
2103
|
+
sheetName: defaultSheetName,
|
|
2104
|
+
key: `item_${counter}`,
|
|
2105
|
+
value
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
return entries;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// src/utils/docIngester.ts
|
|
2113
|
+
async function getDriveExportToken(credentials) {
|
|
2114
|
+
const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
|
|
2115
|
+
const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
|
|
2116
|
+
if (!clientEmail || !privateKey) {
|
|
2117
|
+
throw new Error(
|
|
2118
|
+
"Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
const normalizedKey = privateKey.replace(/\\n/g, "\n");
|
|
2122
|
+
const auth = new import_google_auth_library3.GoogleAuth({
|
|
2123
|
+
credentials: { client_email: clientEmail, private_key: normalizedKey },
|
|
2124
|
+
scopes: ["https://www.googleapis.com/auth/drive.readonly"]
|
|
2125
|
+
});
|
|
2126
|
+
const client = await auth.getClient();
|
|
2127
|
+
const tokenResponse = await client.getAccessToken();
|
|
2128
|
+
return tokenResponse.token;
|
|
2129
|
+
}
|
|
2130
|
+
async function exportDoc(docId, credentials) {
|
|
2131
|
+
const token = await getDriveExportToken(credentials);
|
|
2132
|
+
const base = `https://www.googleapis.com/drive/v3/files/${docId}/export`;
|
|
2133
|
+
const mdRes = await fetch(
|
|
2134
|
+
`${base}?mimeType=text%2Fmarkdown`,
|
|
2135
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
2136
|
+
);
|
|
2137
|
+
if (mdRes.ok) return mdRes.text();
|
|
2138
|
+
const txtRes = await fetch(
|
|
2139
|
+
`${base}?mimeType=text%2Fplain`,
|
|
2140
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
2141
|
+
);
|
|
2142
|
+
if (txtRes.ok) return txtRes.text();
|
|
2143
|
+
const errText = await txtRes.text();
|
|
2144
|
+
throw new Error(
|
|
2145
|
+
`Failed to export doc ${docId}: HTTP ${txtRes.status} \u2013 ${errText}`
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
function entriesToSeedKeys(entries) {
|
|
2149
|
+
const keys = {};
|
|
2150
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2151
|
+
for (const entry of entries) {
|
|
2152
|
+
const base = `${entry.sheetName}.${entry.key}`;
|
|
2153
|
+
const count = (counts.get(base) ?? 0) + 1;
|
|
2154
|
+
counts.set(base, count);
|
|
2155
|
+
const finalKey = count > 1 ? `${base}_${count}` : base;
|
|
2156
|
+
keys[finalKey] = entry.value;
|
|
2157
|
+
}
|
|
2158
|
+
return keys;
|
|
2159
|
+
}
|
|
2160
|
+
function entriesToTranslationData(entries, locale) {
|
|
2161
|
+
const data = {};
|
|
2162
|
+
data[locale] = {};
|
|
2163
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2164
|
+
for (const entry of entries) {
|
|
2165
|
+
const sheetName = entry.sheetName;
|
|
2166
|
+
const entryKey = entry.key;
|
|
2167
|
+
if (sheetName === "__proto__" || sheetName === "constructor" || sheetName === "prototype" || entryKey === "__proto__" || entryKey === "constructor" || entryKey === "prototype") {
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
if (!data[locale][sheetName]) {
|
|
2171
|
+
data[locale][sheetName] = {};
|
|
2172
|
+
}
|
|
2173
|
+
const base = `${sheetName}::${entryKey}`;
|
|
2174
|
+
const count = (counts.get(base) ?? 0) + 1;
|
|
2175
|
+
counts.set(base, count);
|
|
2176
|
+
const finalKey = count > 1 ? `${entryKey}_${count}` : entryKey;
|
|
2177
|
+
data[locale][sheetName][finalKey] = entry.value;
|
|
2178
|
+
}
|
|
2179
|
+
return data;
|
|
2180
|
+
}
|
|
2181
|
+
function docTitle(name) {
|
|
2182
|
+
return name.replace(/_[a-zA-Z]{2,3}(?:[-_][a-zA-Z]{2,4})?$/, "").trim() || name;
|
|
2183
|
+
}
|
|
2184
|
+
async function ingestDoc(docFile, options = {}) {
|
|
2185
|
+
const {
|
|
2186
|
+
targetLocales,
|
|
2187
|
+
keyStrategy = "heading",
|
|
2188
|
+
updateMode = "create-only",
|
|
2189
|
+
credentials,
|
|
2190
|
+
existingEntry,
|
|
2191
|
+
waitSeconds = 1
|
|
2192
|
+
} = options;
|
|
2193
|
+
const sourceLocale = docFile.sourceLocale ?? "en";
|
|
2194
|
+
const entry = existingEntry ? { ...existingEntry, modifiedTime: docFile.modifiedTime } : {
|
|
2195
|
+
id: docFile.id,
|
|
2196
|
+
name: docFile.name,
|
|
2197
|
+
folderPath: docFile.folderPath,
|
|
2198
|
+
generatedFromDoc: true,
|
|
2199
|
+
sourceLocale,
|
|
2200
|
+
modifiedTime: docFile.modifiedTime
|
|
2201
|
+
};
|
|
2202
|
+
const hasLinkedSheet = !!entry.linkedSpreadsheetId;
|
|
2203
|
+
const shouldRefresh = updateMode === "refresh-if-newer" && hasLinkedSheet && !!docFile.modifiedTime && !!existingEntry?.lastIngestedAt && new Date(docFile.modifiedTime) > new Date(existingEntry.lastIngestedAt);
|
|
2204
|
+
if (hasLinkedSheet && !shouldRefresh) {
|
|
2205
|
+
console.log(
|
|
2206
|
+
`[docIngester] Skipping "${docFile.name}" \u2013 linked spreadsheet is already up-to-date.`
|
|
2207
|
+
);
|
|
2208
|
+
return { action: "skipped", entry };
|
|
2209
|
+
}
|
|
2210
|
+
console.log(
|
|
2211
|
+
`[docIngester] Exporting doc "${docFile.name}" (id: ${docFile.id})\u2026`
|
|
2212
|
+
);
|
|
2213
|
+
const content = await exportDoc(docFile.id, credentials);
|
|
2214
|
+
const sheetBaseName = slugifyKey(docTitle(docFile.name)) || "content";
|
|
2215
|
+
const entries = parseDocContent(content, {
|
|
2216
|
+
strategy: keyStrategy,
|
|
2217
|
+
defaultSheetName: sheetBaseName
|
|
2218
|
+
});
|
|
2219
|
+
if (entries.length === 0) {
|
|
2220
|
+
console.warn(
|
|
2221
|
+
`[docIngester] Doc "${docFile.name}" produced no translation entries \u2013 skipping.`
|
|
2222
|
+
);
|
|
2223
|
+
return { action: "skipped", entry };
|
|
2224
|
+
}
|
|
2225
|
+
if (!hasLinkedSheet) {
|
|
2226
|
+
const authClient2 = createAuthClient();
|
|
2227
|
+
const seedKeys = entriesToSeedKeys(entries);
|
|
2228
|
+
const title = docTitle(docFile.name);
|
|
2229
|
+
const { spreadsheetId: spreadsheetId2 } = await createSpreadsheet(authClient2, {
|
|
2230
|
+
title,
|
|
2231
|
+
sourceLocale,
|
|
2232
|
+
targetLocales,
|
|
2233
|
+
seedKeys
|
|
2234
|
+
});
|
|
2235
|
+
entry.linkedSpreadsheetId = spreadsheetId2;
|
|
2236
|
+
entry.lastIngestedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2237
|
+
console.log(
|
|
2238
|
+
`[docIngester] Created spreadsheet ${spreadsheetId2} from doc "${docFile.name}".`
|
|
2239
|
+
);
|
|
2240
|
+
return { action: "created", entry };
|
|
2241
|
+
}
|
|
2242
|
+
const authClient = createAuthClient();
|
|
2243
|
+
const spreadsheetId = entry.linkedSpreadsheetId;
|
|
2244
|
+
const doc = new import_google_spreadsheet3.GoogleSpreadsheet(spreadsheetId, authClient);
|
|
2245
|
+
await doc.loadInfo();
|
|
2246
|
+
const changes = entriesToTranslationData(entries, sourceLocale);
|
|
2247
|
+
await updateSpreadsheetWithLocalChanges(
|
|
2248
|
+
doc,
|
|
2249
|
+
changes,
|
|
2250
|
+
waitSeconds,
|
|
2251
|
+
false,
|
|
2252
|
+
// autoTranslate – formulas already exist in non-base columns
|
|
2253
|
+
{},
|
|
2254
|
+
false
|
|
2255
|
+
);
|
|
2256
|
+
entry.lastIngestedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2257
|
+
console.log(
|
|
2258
|
+
`[docIngester] Refreshed spreadsheet ${spreadsheetId} from doc "${docFile.name}".`
|
|
2259
|
+
);
|
|
2260
|
+
return { action: "refreshed", entry };
|
|
2261
|
+
}
|
|
1904
2262
|
|
|
1905
2263
|
// src/utils/getDriveTranslations.ts
|
|
1906
2264
|
function sanitizeFolderName(name) {
|
|
1907
2265
|
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "sheet";
|
|
1908
2266
|
}
|
|
2267
|
+
async function moveSpreadsheetToFolder(spreadsheetId, folderId) {
|
|
2268
|
+
const clientEmail = process.env.GOOGLE_CLIENT_EMAIL;
|
|
2269
|
+
const rawPrivateKey = process.env.GOOGLE_PRIVATE_KEY;
|
|
2270
|
+
let credentials;
|
|
2271
|
+
if (clientEmail && rawPrivateKey) {
|
|
2272
|
+
credentials = { client_email: clientEmail, private_key: normalizePrivateKey(rawPrivateKey) };
|
|
2273
|
+
}
|
|
2274
|
+
const driveAuth = buildGoogleAuth(
|
|
2275
|
+
["https://www.googleapis.com/auth/drive.file"],
|
|
2276
|
+
credentials
|
|
2277
|
+
);
|
|
2278
|
+
const fileRes = await driveAuth.request({
|
|
2279
|
+
url: `https://www.googleapis.com/drive/v3/files/${spreadsheetId}`,
|
|
2280
|
+
params: { fields: "parents" }
|
|
2281
|
+
});
|
|
2282
|
+
const parentIds = fileRes.data.parents ?? [];
|
|
2283
|
+
await driveAuth.request({
|
|
2284
|
+
url: `https://www.googleapis.com/drive/v3/files/${spreadsheetId}`,
|
|
2285
|
+
method: "PATCH",
|
|
2286
|
+
params: {
|
|
2287
|
+
addParents: folderId,
|
|
2288
|
+
...parentIds.length > 0 ? { removeParents: parentIds.join(",") } : {},
|
|
2289
|
+
fields: "id,parents"
|
|
2290
|
+
}
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
1909
2293
|
async function manageDriveTranslations(options) {
|
|
1910
2294
|
const {
|
|
1911
2295
|
driveFolderId,
|
|
@@ -1923,7 +2307,14 @@ async function manageDriveTranslations(options) {
|
|
|
1923
2307
|
projectName,
|
|
1924
2308
|
domain,
|
|
1925
2309
|
defaultLocale,
|
|
1926
|
-
projectMetadata
|
|
2310
|
+
projectMetadata,
|
|
2311
|
+
// Doc ingestion options
|
|
2312
|
+
scanForDocs = false,
|
|
2313
|
+
docNameFilter,
|
|
2314
|
+
docSourceLocale,
|
|
2315
|
+
docKeyStrategy,
|
|
2316
|
+
docUpdateMode,
|
|
2317
|
+
docTargetLocales
|
|
1927
2318
|
} = options;
|
|
1928
2319
|
if (syncImages && !imageOutputPath) {
|
|
1929
2320
|
throw new Error(
|
|
@@ -1954,6 +2345,36 @@ async function manageDriveTranslations(options) {
|
|
|
1954
2345
|
if (!name) return true;
|
|
1955
2346
|
return spreadsheetNameFilter.test(name);
|
|
1956
2347
|
}) : allIds;
|
|
2348
|
+
if (driveFolderId && filteredIds.length === 0 && translationOptions.autoCreate !== false) {
|
|
2349
|
+
console.log(
|
|
2350
|
+
`[manageDriveTranslations] Drive folder "${driveFolderId}" contains no spreadsheets. Bootstrapping a new spreadsheet\u2026`
|
|
2351
|
+
);
|
|
2352
|
+
const authClient = createAuthClient();
|
|
2353
|
+
const bootstrapTitle = translationOptions.spreadsheetTitle ?? "google-sheet-translations";
|
|
2354
|
+
const created = await createSpreadsheet(authClient, {
|
|
2355
|
+
title: bootstrapTitle,
|
|
2356
|
+
sourceLocale: translationOptions.sourceLocale,
|
|
2357
|
+
targetLocales: translationOptions.targetLocales
|
|
2358
|
+
});
|
|
2359
|
+
console.log(`[manageDriveTranslations] \u2705 Spreadsheet created: ${created.url}`);
|
|
2360
|
+
try {
|
|
2361
|
+
await moveSpreadsheetToFolder(created.spreadsheetId, driveFolderId);
|
|
2362
|
+
console.log(
|
|
2363
|
+
`[manageDriveTranslations] \u2705 Spreadsheet moved into Drive folder "${driveFolderId}"`
|
|
2364
|
+
);
|
|
2365
|
+
} catch (moveErr) {
|
|
2366
|
+
console.warn(
|
|
2367
|
+
`[manageDriveTranslations] \u26A0\uFE0F Could not move spreadsheet into Drive folder:`,
|
|
2368
|
+
moveErr.message
|
|
2369
|
+
);
|
|
2370
|
+
console.warn(
|
|
2371
|
+
` Please move spreadsheet "${created.spreadsheetId}" into folder "${driveFolderId}" manually.`
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
filteredIds.push(created.spreadsheetId);
|
|
2375
|
+
discoveredNames.set(created.spreadsheetId, bootstrapTitle);
|
|
2376
|
+
discoveredFolderPaths.set(created.spreadsheetId, "");
|
|
2377
|
+
}
|
|
1957
2378
|
let translations;
|
|
1958
2379
|
const spreadsheetEntries = [];
|
|
1959
2380
|
const baseOutputDir = translationOptions.translationsOutputDir ?? "translations";
|
|
@@ -2007,8 +2428,48 @@ async function manageDriveTranslations(options) {
|
|
|
2007
2428
|
});
|
|
2008
2429
|
}
|
|
2009
2430
|
let manifest;
|
|
2431
|
+
let docIngestResults;
|
|
2432
|
+
const docEntries = [];
|
|
2433
|
+
const resolvedManifestPath = manifestPath ?? import_node_path8.default.join(baseOutputDir, "i18n-manifest.json");
|
|
2434
|
+
if (driveFolderId && scanForDocs) {
|
|
2435
|
+
const previousManifest = readManifest(resolvedManifestPath);
|
|
2436
|
+
const docScanOptions = {
|
|
2437
|
+
folderId: driveFolderId
|
|
2438
|
+
};
|
|
2439
|
+
const discoveredDocs = await scanDriveFolderForDocs(docScanOptions);
|
|
2440
|
+
console.log(
|
|
2441
|
+
`[manageDriveTranslations] Found ${discoveredDocs.length} doc(s) in Drive folder`
|
|
2442
|
+
);
|
|
2443
|
+
docIngestResults = [];
|
|
2444
|
+
for (const docFile of discoveredDocs) {
|
|
2445
|
+
if (docNameFilter && !docNameFilter.test(docFile.name)) continue;
|
|
2446
|
+
if (!docFile.sourceLocale && docSourceLocale) {
|
|
2447
|
+
docFile.sourceLocale = docSourceLocale;
|
|
2448
|
+
}
|
|
2449
|
+
const existingEntry = previousManifest?.docs?.find(
|
|
2450
|
+
(d) => d.id === docFile.id
|
|
2451
|
+
);
|
|
2452
|
+
const ingesterOptions = {
|
|
2453
|
+
targetLocales: docTargetLocales,
|
|
2454
|
+
keyStrategy: docKeyStrategy,
|
|
2455
|
+
updateMode: docUpdateMode,
|
|
2456
|
+
existingEntry,
|
|
2457
|
+
waitSeconds: translationOptions.waitSeconds
|
|
2458
|
+
};
|
|
2459
|
+
try {
|
|
2460
|
+
const result = await ingestDoc(docFile, ingesterOptions);
|
|
2461
|
+
docEntries.push(result.entry);
|
|
2462
|
+
docIngestResults.push({ docName: docFile.name, action: result.action });
|
|
2463
|
+
} catch (err) {
|
|
2464
|
+
console.error(
|
|
2465
|
+
`[manageDriveTranslations] Failed to ingest doc "${docFile.name}":`,
|
|
2466
|
+
err
|
|
2467
|
+
);
|
|
2468
|
+
if (existingEntry) docEntries.push(existingEntry);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2010
2472
|
if (shouldCreateManifest) {
|
|
2011
|
-
const resolvedManifestPath = manifestPath ?? import_node_path8.default.join(baseOutputDir, "i18n-manifest.json");
|
|
2012
2473
|
manifest = buildManifest({
|
|
2013
2474
|
translations,
|
|
2014
2475
|
spreadsheets: spreadsheetEntries,
|
|
@@ -2017,11 +2478,12 @@ async function manageDriveTranslations(options) {
|
|
|
2017
2478
|
projectName,
|
|
2018
2479
|
domain,
|
|
2019
2480
|
defaultLocale,
|
|
2020
|
-
projectMetadata
|
|
2481
|
+
projectMetadata,
|
|
2482
|
+
docs: docEntries.length > 0 ? docEntries : void 0
|
|
2021
2483
|
});
|
|
2022
2484
|
writeManifest(manifest, resolvedManifestPath);
|
|
2023
2485
|
}
|
|
2024
|
-
return { translations, spreadsheetIds: filteredIds, imageSync, manifest };
|
|
2486
|
+
return { translations, spreadsheetIds: filteredIds, imageSync, manifest, docIngestResults };
|
|
2025
2487
|
}
|
|
2026
2488
|
|
|
2027
2489
|
// src/index.ts
|
|
@@ -2037,6 +2499,9 @@ var index_default = getSpreadSheetData;
|
|
|
2037
2499
|
createAuthClient,
|
|
2038
2500
|
createLocaleMapping,
|
|
2039
2501
|
createSpreadsheet,
|
|
2502
|
+
entriesToSeedKeys,
|
|
2503
|
+
entriesToTranslationData,
|
|
2504
|
+
exportDoc,
|
|
2040
2505
|
filterValidLocales,
|
|
2041
2506
|
findLocalChanges,
|
|
2042
2507
|
getGoogleTranslateCode,
|
|
@@ -2048,6 +2513,8 @@ var index_default = getSpreadSheetData;
|
|
|
2048
2513
|
getSpreadSheetData,
|
|
2049
2514
|
getTranslationSummary,
|
|
2050
2515
|
handleBidirectionalSync,
|
|
2516
|
+
inferLocaleFromDocName,
|
|
2517
|
+
ingestDoc,
|
|
2051
2518
|
isValidLocale,
|
|
2052
2519
|
manageDriveTranslations,
|
|
2053
2520
|
mergeMultipleTranslationData,
|
|
@@ -2055,10 +2522,14 @@ var index_default = getSpreadSheetData;
|
|
|
2055
2522
|
normalizeExtension,
|
|
2056
2523
|
normalizeLocaleCode,
|
|
2057
2524
|
normalizePrivateKey,
|
|
2525
|
+
parseDocContent,
|
|
2058
2526
|
processRawRows,
|
|
2527
|
+
readManifest,
|
|
2059
2528
|
readPublicSheet,
|
|
2060
2529
|
resolveLocaleWithFallback,
|
|
2530
|
+
scanDriveFolderForDocs,
|
|
2061
2531
|
scanDriveFolderForSpreadsheets,
|
|
2532
|
+
slugifyKey,
|
|
2062
2533
|
syncDriveImages,
|
|
2063
2534
|
updateSpreadsheetWithLocalChanges,
|
|
2064
2535
|
validateCredentials,
|