@el-j/google-sheet-translations 2.2.0-beta.2 → 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/index.js CHANGED
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  DEFAULT_IMAGE_EXTENSIONS: () => DEFAULT_IMAGE_EXTENSIONS,
34
34
  DEFAULT_WAIT_SECONDS: () => DEFAULT_WAIT_SECONDS,
35
+ buildGoogleAuth: () => buildGoogleAuth,
35
36
  buildManifest: () => buildManifest,
36
37
  convertFromDataJsonFormat: () => convertFromDataJsonFormat,
37
38
  convertToDataJsonFormat: () => convertToDataJsonFormat,
@@ -39,6 +40,9 @@ __export(index_exports, {
39
40
  createLocaleMapping: () => createLocaleMapping,
40
41
  createSpreadsheet: () => createSpreadsheet,
41
42
  default: () => index_default,
43
+ entriesToSeedKeys: () => entriesToSeedKeys,
44
+ entriesToTranslationData: () => entriesToTranslationData,
45
+ exportDoc: () => exportDoc,
42
46
  filterValidLocales: () => filterValidLocales,
43
47
  findLocalChanges: () => findLocalChanges,
44
48
  getGoogleTranslateCode: () => getGoogleTranslateCode,
@@ -50,16 +54,23 @@ __export(index_exports, {
50
54
  getSpreadSheetData: () => getSpreadSheetData,
51
55
  getTranslationSummary: () => getTranslationSummary,
52
56
  handleBidirectionalSync: () => handleBidirectionalSync,
57
+ inferLocaleFromDocName: () => inferLocaleFromDocName,
58
+ ingestDoc: () => ingestDoc,
53
59
  isValidLocale: () => isValidLocale,
54
60
  manageDriveTranslations: () => manageDriveTranslations,
55
61
  mergeMultipleTranslationData: () => mergeMultipleTranslationData,
56
62
  mergeSheets: () => mergeSheets,
57
63
  normalizeExtension: () => normalizeExtension,
58
64
  normalizeLocaleCode: () => normalizeLocaleCode,
65
+ normalizePrivateKey: () => normalizePrivateKey,
66
+ parseDocContent: () => parseDocContent,
59
67
  processRawRows: () => processRawRows,
68
+ readManifest: () => readManifest,
60
69
  readPublicSheet: () => readPublicSheet,
61
70
  resolveLocaleWithFallback: () => resolveLocaleWithFallback,
71
+ scanDriveFolderForDocs: () => scanDriveFolderForDocs,
62
72
  scanDriveFolderForSpreadsheets: () => scanDriveFolderForSpreadsheets,
73
+ slugifyKey: () => slugifyKey,
63
74
  syncDriveImages: () => syncDriveImages,
64
75
  updateSpreadsheetWithLocalChanges: () => updateSpreadsheetWithLocalChanges,
65
76
  validateCredentials: () => validateCredentials,
@@ -85,13 +96,20 @@ var import_google_auth_library = require("google-auth-library");
85
96
 
86
97
  // src/utils/validateEnv.ts
87
98
  function validateCredentials() {
99
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
100
+ return {
101
+ GOOGLE_CLIENT_EMAIL: process.env.GOOGLE_CLIENT_EMAIL ?? "",
102
+ GOOGLE_PRIVATE_KEY: process.env.GOOGLE_PRIVATE_KEY ?? ""
103
+ };
104
+ }
88
105
  const requiredVars = ["GOOGLE_CLIENT_EMAIL", "GOOGLE_PRIVATE_KEY"];
89
106
  const missing = requiredVars.filter((v) => !process.env[v]);
90
107
  if (missing.length > 0) {
91
108
  throw new Error(
92
109
  `Missing required environment variables: ${missing.join(", ")}
93
110
 
94
- Make sure these are set in your .env file or environment.`
111
+ Make sure these are set in your .env file or environment.
112
+ Alternatively, set GOOGLE_APPLICATION_CREDENTIALS for Workload Identity Federation.`
95
113
  );
96
114
  }
97
115
  return {
@@ -100,6 +118,21 @@ Make sure these are set in your .env file or environment.`
100
118
  };
101
119
  }
102
120
  function validateEnv() {
121
+ const spreadsheetId = process.env.GOOGLE_SPREADSHEET_ID;
122
+ if (!spreadsheetId) {
123
+ throw new Error(
124
+ `Missing required environment variable: GOOGLE_SPREADSHEET_ID
125
+
126
+ Make sure this is set in your .env file or environment.`
127
+ );
128
+ }
129
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
130
+ return {
131
+ GOOGLE_CLIENT_EMAIL: process.env.GOOGLE_CLIENT_EMAIL ?? "",
132
+ GOOGLE_PRIVATE_KEY: process.env.GOOGLE_PRIVATE_KEY ?? "",
133
+ GOOGLE_SPREADSHEET_ID: spreadsheetId
134
+ };
135
+ }
103
136
  const requiredVars = [
104
137
  "GOOGLE_CLIENT_EMAIL",
105
138
  "GOOGLE_PRIVATE_KEY",
@@ -110,24 +143,43 @@ function validateEnv() {
110
143
  throw new Error(
111
144
  `Missing required environment variables: ${missingVars.join(", ")}
112
145
 
113
- Make sure these are set in your .env file or environment.`
146
+ Make sure these are set in your .env file or environment.
147
+ Alternatively, set GOOGLE_APPLICATION_CREDENTIALS for Workload Identity Federation.`
114
148
  );
115
149
  }
116
150
  return {
117
151
  GOOGLE_CLIENT_EMAIL: process.env.GOOGLE_CLIENT_EMAIL,
118
152
  GOOGLE_PRIVATE_KEY: process.env.GOOGLE_PRIVATE_KEY,
119
- GOOGLE_SPREADSHEET_ID: process.env.GOOGLE_SPREADSHEET_ID
153
+ GOOGLE_SPREADSHEET_ID: spreadsheetId
120
154
  };
121
155
  }
122
156
 
123
157
  // src/utils/auth.ts
158
+ function normalizePrivateKey(key) {
159
+ let normalized = key;
160
+ const outer = key.trim();
161
+ if (outer.startsWith('"') && outer.endsWith('"') || outer.startsWith("'") && outer.endsWith("'")) {
162
+ normalized = outer.slice(1, -1);
163
+ }
164
+ normalized = normalized.replace(/\\n/g, "\n");
165
+ normalized = normalized.replace(/\r\n/g, "\n");
166
+ return normalized;
167
+ }
168
+ function buildGoogleAuth(scopes, credentials) {
169
+ if (credentials) {
170
+ return new import_google_auth_library.GoogleAuth({ credentials, scopes });
171
+ }
172
+ return new import_google_auth_library.GoogleAuth({ scopes });
173
+ }
124
174
  function createAuthClient() {
175
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
176
+ return buildGoogleAuth(["https://www.googleapis.com/auth/spreadsheets"]);
177
+ }
125
178
  const { GOOGLE_CLIENT_EMAIL, GOOGLE_PRIVATE_KEY } = validateCredentials();
126
- const normalizedKey = GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n");
127
- return new import_google_auth_library.JWT({
128
- email: GOOGLE_CLIENT_EMAIL,
129
- key: normalizedKey,
130
- scopes: ["https://www.googleapis.com/auth/spreadsheets"]
179
+ const normalizedKey = normalizePrivateKey(GOOGLE_PRIVATE_KEY);
180
+ return buildGoogleAuth(["https://www.googleapis.com/auth/spreadsheets"], {
181
+ client_email: GOOGLE_CLIENT_EMAIL,
182
+ private_key: normalizedKey
131
183
  });
132
184
  }
133
185
 
@@ -1114,7 +1166,6 @@ async function createSpreadsheet(authClient, options = {}) {
1114
1166
  targetLocales = DEFAULT_TARGET_LOCALES,
1115
1167
  seedKeys = STARTER_KEYS
1116
1168
  } = options;
1117
- await authClient.authorize();
1118
1169
  const createRes = await withRetry(
1119
1170
  () => authClient.request({
1120
1171
  url: "https://sheets.googleapis.com/v4/spreadsheets",
@@ -1225,13 +1276,13 @@ GOOGLE_SPREADSHEET_ID=${id}
1225
1276
  async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
1226
1277
  const config = normalizeConfig(options);
1227
1278
  const baseDelayMs = config.waitSeconds * 1e3;
1228
- const docTitle = _docTitle ?? [];
1229
- if (docTitle.length === 0) {
1279
+ const docTitle2 = _docTitle ?? [];
1280
+ if (docTitle2.length === 0) {
1230
1281
  console.warn("No sheet titles provided, cannot process spreadsheet data");
1231
1282
  return {};
1232
1283
  }
1233
- if (!docTitle.includes("i18n")) {
1234
- docTitle.push("i18n");
1284
+ if (!docTitle2.includes("i18n")) {
1285
+ docTitle2.push("i18n");
1235
1286
  }
1236
1287
  const finalTranslations = {};
1237
1288
  const allLocales = /* @__PURE__ */ new Set();
@@ -1268,9 +1319,9 @@ async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
1268
1319
  "No spreadsheet ID provided. Set GOOGLE_SPREADSHEET_ID or pass spreadsheetId in options."
1269
1320
  );
1270
1321
  }
1271
- console.log(`Processing ${docTitle.length} sheets: ${docTitle.join(", ")}`);
1322
+ console.log(`Processing ${docTitle2.length} sheets: ${docTitle2.join(", ")}`);
1272
1323
  await Promise.all(
1273
- docTitle.map(async (title) => {
1324
+ docTitle2.map(async (title) => {
1274
1325
  let rows;
1275
1326
  try {
1276
1327
  rows = await withRetry(
@@ -1303,7 +1354,7 @@ async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
1303
1354
  );
1304
1355
  }
1305
1356
  }
1306
- console.log(`Processing ${docTitle.length} sheets: ${docTitle.join(", ")}`);
1357
+ console.log(`Processing ${docTitle2.length} sheets: ${docTitle2.join(", ")}`);
1307
1358
  const doc = new import_google_spreadsheet2.GoogleSpreadsheet(spreadsheetId, serviceAuthClient);
1308
1359
  try {
1309
1360
  await withRetry(() => doc.loadInfo(true), "loadInfo", baseDelayMs);
@@ -1311,7 +1362,7 @@ async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
1311
1362
  throw new Error(`Failed to load spreadsheet "${spreadsheetId}"`, { cause: err });
1312
1363
  }
1313
1364
  await Promise.all(
1314
- docTitle.map(async (title) => {
1365
+ docTitle2.map(async (title) => {
1315
1366
  const sheet = doc.sheetsByTitle[title];
1316
1367
  if (!sheet) {
1317
1368
  console.warn(`Sheet "${title}" not found in the document`);
@@ -1436,23 +1487,22 @@ async function getMultipleSpreadSheetsData(docTitles, options = {}) {
1436
1487
  }
1437
1488
 
1438
1489
  // src/utils/driveFolderScanner.ts
1439
- var import_google_auth_library2 = require("google-auth-library");
1440
1490
  var SPREADSHEET_MIME = "application/vnd.google-apps.spreadsheet";
1441
1491
  var FOLDER_MIME = "application/vnd.google-apps.folder";
1442
1492
  var DRIVE_FILES_URL = "https://www.googleapis.com/drive/v3/files";
1493
+ var DRIVE_SCOPES = ["https://www.googleapis.com/auth/drive.readonly"];
1443
1494
  async function getAccessToken(credentials) {
1444
1495
  const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1445
1496
  const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
1446
- if (!clientEmail || !privateKey) {
1497
+ let driveCredentials;
1498
+ if (clientEmail && privateKey) {
1499
+ driveCredentials = { client_email: clientEmail, private_key: normalizePrivateKey(privateKey) };
1500
+ } else if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
1447
1501
  throw new Error(
1448
- "Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
1502
+ "Google Drive credentials required: set GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY, or set GOOGLE_APPLICATION_CREDENTIALS for Workload Identity Federation."
1449
1503
  );
1450
1504
  }
1451
- const normalizedKey = privateKey.replace(/\\n/g, "\n");
1452
- const auth = new import_google_auth_library2.GoogleAuth({
1453
- credentials: { client_email: clientEmail, private_key: normalizedKey },
1454
- scopes: ["https://www.googleapis.com/auth/drive.readonly"]
1455
- });
1505
+ const auth = buildGoogleAuth(DRIVE_SCOPES, driveCredentials);
1456
1506
  const client = await auth.getClient();
1457
1507
  const tokenResponse = await client.getAccessToken();
1458
1508
  return tokenResponse.token;
@@ -1527,7 +1577,6 @@ var import_node_fs6 = require("node:fs");
1527
1577
  var import_node_path5 = require("node:path");
1528
1578
  var import_promises4 = require("node:stream/promises");
1529
1579
  var import_node_stream = require("node:stream");
1530
- var import_google_auth_library3 = require("google-auth-library");
1531
1580
  var DEFAULT_IMAGE_MIME_TYPES = [
1532
1581
  "image/jpeg",
1533
1582
  "image/jpg",
@@ -1551,19 +1600,19 @@ function normalizeExtension(name) {
1551
1600
  if (ext === "jpeg") ext = "jpg";
1552
1601
  return `${base}.${ext}`;
1553
1602
  }
1603
+ var DRIVE_SCOPES2 = ["https://www.googleapis.com/auth/drive.readonly"];
1554
1604
  async function getAccessToken2(credentials) {
1555
1605
  const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1556
1606
  const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
1557
- if (!clientEmail || !privateKey) {
1607
+ let driveCredentials;
1608
+ if (clientEmail && privateKey) {
1609
+ driveCredentials = { client_email: clientEmail, private_key: normalizePrivateKey(privateKey) };
1610
+ } else if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
1558
1611
  throw new Error(
1559
- "Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
1612
+ "Google Drive credentials required: set GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY, or set GOOGLE_APPLICATION_CREDENTIALS for Workload Identity Federation."
1560
1613
  );
1561
1614
  }
1562
- const normalizedKey = privateKey.replace(/\\n/g, "\n");
1563
- const auth = new import_google_auth_library3.GoogleAuth({
1564
- credentials: { client_email: clientEmail, private_key: normalizedKey },
1565
- scopes: ["https://www.googleapis.com/auth/drive.readonly"]
1566
- });
1615
+ const auth = buildGoogleAuth(DRIVE_SCOPES2, driveCredentials);
1567
1616
  const client = await auth.getClient();
1568
1617
  const tokenResponse = await client.getAccessToken();
1569
1618
  return tokenResponse.token;
@@ -1848,6 +1897,7 @@ function buildManifest(options) {
1848
1897
  locales,
1849
1898
  defaultLocale: options.defaultLocale,
1850
1899
  spreadsheets: options.spreadsheets,
1900
+ docs: options.docs,
1851
1901
  outputDirectory: options.outputDirectory,
1852
1902
  flatten: options.flatten,
1853
1903
  projectMetadata: options.projectMetadata
@@ -1861,11 +1911,385 @@ function writeManifest(manifest, manifestPath) {
1861
1911
  import_node_fs8.default.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
1862
1912
  console.log(`[driveProjectIndex] Wrote project manifest \u2192 ${manifestPath}`);
1863
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
+ }
1864
2262
 
1865
2263
  // src/utils/getDriveTranslations.ts
1866
2264
  function sanitizeFolderName(name) {
1867
2265
  return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "sheet";
1868
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
+ }
1869
2293
  async function manageDriveTranslations(options) {
1870
2294
  const {
1871
2295
  driveFolderId,
@@ -1883,7 +2307,14 @@ async function manageDriveTranslations(options) {
1883
2307
  projectName,
1884
2308
  domain,
1885
2309
  defaultLocale,
1886
- projectMetadata
2310
+ projectMetadata,
2311
+ // Doc ingestion options
2312
+ scanForDocs = false,
2313
+ docNameFilter,
2314
+ docSourceLocale,
2315
+ docKeyStrategy,
2316
+ docUpdateMode,
2317
+ docTargetLocales
1887
2318
  } = options;
1888
2319
  if (syncImages && !imageOutputPath) {
1889
2320
  throw new Error(
@@ -1914,6 +2345,36 @@ async function manageDriveTranslations(options) {
1914
2345
  if (!name) return true;
1915
2346
  return spreadsheetNameFilter.test(name);
1916
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
+ }
1917
2378
  let translations;
1918
2379
  const spreadsheetEntries = [];
1919
2380
  const baseOutputDir = translationOptions.translationsOutputDir ?? "translations";
@@ -1967,8 +2428,48 @@ async function manageDriveTranslations(options) {
1967
2428
  });
1968
2429
  }
1969
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
+ }
1970
2472
  if (shouldCreateManifest) {
1971
- const resolvedManifestPath = manifestPath ?? import_node_path8.default.join(baseOutputDir, "i18n-manifest.json");
1972
2473
  manifest = buildManifest({
1973
2474
  translations,
1974
2475
  spreadsheets: spreadsheetEntries,
@@ -1977,11 +2478,12 @@ async function manageDriveTranslations(options) {
1977
2478
  projectName,
1978
2479
  domain,
1979
2480
  defaultLocale,
1980
- projectMetadata
2481
+ projectMetadata,
2482
+ docs: docEntries.length > 0 ? docEntries : void 0
1981
2483
  });
1982
2484
  writeManifest(manifest, resolvedManifestPath);
1983
2485
  }
1984
- return { translations, spreadsheetIds: filteredIds, imageSync, manifest };
2486
+ return { translations, spreadsheetIds: filteredIds, imageSync, manifest, docIngestResults };
1985
2487
  }
1986
2488
 
1987
2489
  // src/index.ts
@@ -1990,12 +2492,16 @@ var index_default = getSpreadSheetData;
1990
2492
  0 && (module.exports = {
1991
2493
  DEFAULT_IMAGE_EXTENSIONS,
1992
2494
  DEFAULT_WAIT_SECONDS,
2495
+ buildGoogleAuth,
1993
2496
  buildManifest,
1994
2497
  convertFromDataJsonFormat,
1995
2498
  convertToDataJsonFormat,
1996
2499
  createAuthClient,
1997
2500
  createLocaleMapping,
1998
2501
  createSpreadsheet,
2502
+ entriesToSeedKeys,
2503
+ entriesToTranslationData,
2504
+ exportDoc,
1999
2505
  filterValidLocales,
2000
2506
  findLocalChanges,
2001
2507
  getGoogleTranslateCode,
@@ -2007,16 +2513,23 @@ var index_default = getSpreadSheetData;
2007
2513
  getSpreadSheetData,
2008
2514
  getTranslationSummary,
2009
2515
  handleBidirectionalSync,
2516
+ inferLocaleFromDocName,
2517
+ ingestDoc,
2010
2518
  isValidLocale,
2011
2519
  manageDriveTranslations,
2012
2520
  mergeMultipleTranslationData,
2013
2521
  mergeSheets,
2014
2522
  normalizeExtension,
2015
2523
  normalizeLocaleCode,
2524
+ normalizePrivateKey,
2525
+ parseDocContent,
2016
2526
  processRawRows,
2527
+ readManifest,
2017
2528
  readPublicSheet,
2018
2529
  resolveLocaleWithFallback,
2530
+ scanDriveFolderForDocs,
2019
2531
  scanDriveFolderForSpreadsheets,
2532
+ slugifyKey,
2020
2533
  syncDriveImages,
2021
2534
  updateSpreadsheetWithLocalChanges,
2022
2535
  validateCredentials,