@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/esm/index.js CHANGED
@@ -4,17 +4,24 @@ import path4 from "node:path";
4
4
  import { GoogleSpreadsheet as GoogleSpreadsheet2 } from "google-spreadsheet";
5
5
 
6
6
  // src/utils/auth.ts
7
- import { JWT } from "google-auth-library";
7
+ import { GoogleAuth } from "google-auth-library";
8
8
 
9
9
  // src/utils/validateEnv.ts
10
10
  function validateCredentials() {
11
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
12
+ return {
13
+ GOOGLE_CLIENT_EMAIL: process.env.GOOGLE_CLIENT_EMAIL ?? "",
14
+ GOOGLE_PRIVATE_KEY: process.env.GOOGLE_PRIVATE_KEY ?? ""
15
+ };
16
+ }
11
17
  const requiredVars = ["GOOGLE_CLIENT_EMAIL", "GOOGLE_PRIVATE_KEY"];
12
18
  const missing = requiredVars.filter((v) => !process.env[v]);
13
19
  if (missing.length > 0) {
14
20
  throw new Error(
15
21
  `Missing required environment variables: ${missing.join(", ")}
16
22
 
17
- Make sure these are set in your .env file or environment.`
23
+ Make sure these are set in your .env file or environment.
24
+ Alternatively, set GOOGLE_APPLICATION_CREDENTIALS for Workload Identity Federation.`
18
25
  );
19
26
  }
20
27
  return {
@@ -23,6 +30,21 @@ Make sure these are set in your .env file or environment.`
23
30
  };
24
31
  }
25
32
  function validateEnv() {
33
+ const spreadsheetId = process.env.GOOGLE_SPREADSHEET_ID;
34
+ if (!spreadsheetId) {
35
+ throw new Error(
36
+ `Missing required environment variable: GOOGLE_SPREADSHEET_ID
37
+
38
+ Make sure this is set in your .env file or environment.`
39
+ );
40
+ }
41
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
42
+ return {
43
+ GOOGLE_CLIENT_EMAIL: process.env.GOOGLE_CLIENT_EMAIL ?? "",
44
+ GOOGLE_PRIVATE_KEY: process.env.GOOGLE_PRIVATE_KEY ?? "",
45
+ GOOGLE_SPREADSHEET_ID: spreadsheetId
46
+ };
47
+ }
26
48
  const requiredVars = [
27
49
  "GOOGLE_CLIENT_EMAIL",
28
50
  "GOOGLE_PRIVATE_KEY",
@@ -33,24 +55,43 @@ function validateEnv() {
33
55
  throw new Error(
34
56
  `Missing required environment variables: ${missingVars.join(", ")}
35
57
 
36
- Make sure these are set in your .env file or environment.`
58
+ Make sure these are set in your .env file or environment.
59
+ Alternatively, set GOOGLE_APPLICATION_CREDENTIALS for Workload Identity Federation.`
37
60
  );
38
61
  }
39
62
  return {
40
63
  GOOGLE_CLIENT_EMAIL: process.env.GOOGLE_CLIENT_EMAIL,
41
64
  GOOGLE_PRIVATE_KEY: process.env.GOOGLE_PRIVATE_KEY,
42
- GOOGLE_SPREADSHEET_ID: process.env.GOOGLE_SPREADSHEET_ID
65
+ GOOGLE_SPREADSHEET_ID: spreadsheetId
43
66
  };
44
67
  }
45
68
 
46
69
  // src/utils/auth.ts
70
+ function normalizePrivateKey(key) {
71
+ let normalized = key;
72
+ const outer = key.trim();
73
+ if (outer.startsWith('"') && outer.endsWith('"') || outer.startsWith("'") && outer.endsWith("'")) {
74
+ normalized = outer.slice(1, -1);
75
+ }
76
+ normalized = normalized.replace(/\\n/g, "\n");
77
+ normalized = normalized.replace(/\r\n/g, "\n");
78
+ return normalized;
79
+ }
80
+ function buildGoogleAuth(scopes, credentials) {
81
+ if (credentials) {
82
+ return new GoogleAuth({ credentials, scopes });
83
+ }
84
+ return new GoogleAuth({ scopes });
85
+ }
47
86
  function createAuthClient() {
87
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
88
+ return buildGoogleAuth(["https://www.googleapis.com/auth/spreadsheets"]);
89
+ }
48
90
  const { GOOGLE_CLIENT_EMAIL, GOOGLE_PRIVATE_KEY } = validateCredentials();
49
- const normalizedKey = GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n");
50
- return new JWT({
51
- email: GOOGLE_CLIENT_EMAIL,
52
- key: normalizedKey,
53
- scopes: ["https://www.googleapis.com/auth/spreadsheets"]
91
+ const normalizedKey = normalizePrivateKey(GOOGLE_PRIVATE_KEY);
92
+ return buildGoogleAuth(["https://www.googleapis.com/auth/spreadsheets"], {
93
+ client_email: GOOGLE_CLIENT_EMAIL,
94
+ private_key: normalizedKey
54
95
  });
55
96
  }
56
97
 
@@ -1037,7 +1078,6 @@ async function createSpreadsheet(authClient, options = {}) {
1037
1078
  targetLocales = DEFAULT_TARGET_LOCALES,
1038
1079
  seedKeys = STARTER_KEYS
1039
1080
  } = options;
1040
- await authClient.authorize();
1041
1081
  const createRes = await withRetry(
1042
1082
  () => authClient.request({
1043
1083
  url: "https://sheets.googleapis.com/v4/spreadsheets",
@@ -1148,13 +1188,13 @@ GOOGLE_SPREADSHEET_ID=${id}
1148
1188
  async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
1149
1189
  const config = normalizeConfig(options);
1150
1190
  const baseDelayMs = config.waitSeconds * 1e3;
1151
- const docTitle = _docTitle ?? [];
1152
- if (docTitle.length === 0) {
1191
+ const docTitle2 = _docTitle ?? [];
1192
+ if (docTitle2.length === 0) {
1153
1193
  console.warn("No sheet titles provided, cannot process spreadsheet data");
1154
1194
  return {};
1155
1195
  }
1156
- if (!docTitle.includes("i18n")) {
1157
- docTitle.push("i18n");
1196
+ if (!docTitle2.includes("i18n")) {
1197
+ docTitle2.push("i18n");
1158
1198
  }
1159
1199
  const finalTranslations = {};
1160
1200
  const allLocales = /* @__PURE__ */ new Set();
@@ -1191,9 +1231,9 @@ async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
1191
1231
  "No spreadsheet ID provided. Set GOOGLE_SPREADSHEET_ID or pass spreadsheetId in options."
1192
1232
  );
1193
1233
  }
1194
- console.log(`Processing ${docTitle.length} sheets: ${docTitle.join(", ")}`);
1234
+ console.log(`Processing ${docTitle2.length} sheets: ${docTitle2.join(", ")}`);
1195
1235
  await Promise.all(
1196
- docTitle.map(async (title) => {
1236
+ docTitle2.map(async (title) => {
1197
1237
  let rows;
1198
1238
  try {
1199
1239
  rows = await withRetry(
@@ -1226,7 +1266,7 @@ async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
1226
1266
  );
1227
1267
  }
1228
1268
  }
1229
- console.log(`Processing ${docTitle.length} sheets: ${docTitle.join(", ")}`);
1269
+ console.log(`Processing ${docTitle2.length} sheets: ${docTitle2.join(", ")}`);
1230
1270
  const doc = new GoogleSpreadsheet2(spreadsheetId, serviceAuthClient);
1231
1271
  try {
1232
1272
  await withRetry(() => doc.loadInfo(true), "loadInfo", baseDelayMs);
@@ -1234,7 +1274,7 @@ async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
1234
1274
  throw new Error(`Failed to load spreadsheet "${spreadsheetId}"`, { cause: err });
1235
1275
  }
1236
1276
  await Promise.all(
1237
- docTitle.map(async (title) => {
1277
+ docTitle2.map(async (title) => {
1238
1278
  const sheet = doc.sheetsByTitle[title];
1239
1279
  if (!sheet) {
1240
1280
  console.warn(`Sheet "${title}" not found in the document`);
@@ -1359,23 +1399,22 @@ async function getMultipleSpreadSheetsData(docTitles, options = {}) {
1359
1399
  }
1360
1400
 
1361
1401
  // src/utils/driveFolderScanner.ts
1362
- import { GoogleAuth } from "google-auth-library";
1363
1402
  var SPREADSHEET_MIME = "application/vnd.google-apps.spreadsheet";
1364
1403
  var FOLDER_MIME = "application/vnd.google-apps.folder";
1365
1404
  var DRIVE_FILES_URL = "https://www.googleapis.com/drive/v3/files";
1405
+ var DRIVE_SCOPES = ["https://www.googleapis.com/auth/drive.readonly"];
1366
1406
  async function getAccessToken(credentials) {
1367
1407
  const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1368
1408
  const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
1369
- if (!clientEmail || !privateKey) {
1409
+ let driveCredentials;
1410
+ if (clientEmail && privateKey) {
1411
+ driveCredentials = { client_email: clientEmail, private_key: normalizePrivateKey(privateKey) };
1412
+ } else if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
1370
1413
  throw new Error(
1371
- "Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
1414
+ "Google Drive credentials required: set GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY, or set GOOGLE_APPLICATION_CREDENTIALS for Workload Identity Federation."
1372
1415
  );
1373
1416
  }
1374
- const normalizedKey = privateKey.replace(/\\n/g, "\n");
1375
- const auth = new GoogleAuth({
1376
- credentials: { client_email: clientEmail, private_key: normalizedKey },
1377
- scopes: ["https://www.googleapis.com/auth/drive.readonly"]
1378
- });
1417
+ const auth = buildGoogleAuth(DRIVE_SCOPES, driveCredentials);
1379
1418
  const client = await auth.getClient();
1380
1419
  const tokenResponse = await client.getAccessToken();
1381
1420
  return tokenResponse.token;
@@ -1450,7 +1489,6 @@ import { createWriteStream, mkdirSync, existsSync, readdirSync, unlinkSync, stat
1450
1489
  import { join, dirname } from "node:path";
1451
1490
  import { pipeline } from "node:stream/promises";
1452
1491
  import { Readable } from "node:stream";
1453
- import { GoogleAuth as GoogleAuth2 } from "google-auth-library";
1454
1492
  var DEFAULT_IMAGE_MIME_TYPES = [
1455
1493
  "image/jpeg",
1456
1494
  "image/jpg",
@@ -1474,19 +1512,19 @@ function normalizeExtension(name) {
1474
1512
  if (ext === "jpeg") ext = "jpg";
1475
1513
  return `${base}.${ext}`;
1476
1514
  }
1515
+ var DRIVE_SCOPES2 = ["https://www.googleapis.com/auth/drive.readonly"];
1477
1516
  async function getAccessToken2(credentials) {
1478
1517
  const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1479
1518
  const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
1480
- if (!clientEmail || !privateKey) {
1519
+ let driveCredentials;
1520
+ if (clientEmail && privateKey) {
1521
+ driveCredentials = { client_email: clientEmail, private_key: normalizePrivateKey(privateKey) };
1522
+ } else if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
1481
1523
  throw new Error(
1482
- "Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
1524
+ "Google Drive credentials required: set GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY, or set GOOGLE_APPLICATION_CREDENTIALS for Workload Identity Federation."
1483
1525
  );
1484
1526
  }
1485
- const normalizedKey = privateKey.replace(/\\n/g, "\n");
1486
- const auth = new GoogleAuth2({
1487
- credentials: { client_email: clientEmail, private_key: normalizedKey },
1488
- scopes: ["https://www.googleapis.com/auth/drive.readonly"]
1489
- });
1527
+ const auth = buildGoogleAuth(DRIVE_SCOPES2, driveCredentials);
1490
1528
  const client = await auth.getClient();
1491
1529
  const tokenResponse = await client.getAccessToken();
1492
1530
  return tokenResponse.token;
@@ -1771,6 +1809,7 @@ function buildManifest(options) {
1771
1809
  locales,
1772
1810
  defaultLocale: options.defaultLocale,
1773
1811
  spreadsheets: options.spreadsheets,
1812
+ docs: options.docs,
1774
1813
  outputDirectory: options.outputDirectory,
1775
1814
  flatten: options.flatten,
1776
1815
  projectMetadata: options.projectMetadata
@@ -1784,11 +1823,385 @@ function writeManifest(manifest, manifestPath) {
1784
1823
  fs6.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
1785
1824
  console.log(`[driveProjectIndex] Wrote project manifest \u2192 ${manifestPath}`);
1786
1825
  }
1826
+ function readManifest(manifestPath) {
1827
+ try {
1828
+ const content = fs6.readFileSync(manifestPath, "utf8");
1829
+ return JSON.parse(content);
1830
+ } catch {
1831
+ return void 0;
1832
+ }
1833
+ }
1834
+
1835
+ // src/utils/driveDocScanner.ts
1836
+ import { GoogleAuth as GoogleAuth2 } from "google-auth-library";
1837
+ var DOC_MIME = "application/vnd.google-apps.document";
1838
+ var FOLDER_MIME3 = "application/vnd.google-apps.folder";
1839
+ var DRIVE_FILES_URL3 = "https://www.googleapis.com/drive/v3/files";
1840
+ async function getAccessToken3(credentials) {
1841
+ const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
1842
+ const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
1843
+ if (!clientEmail || !privateKey) {
1844
+ throw new Error(
1845
+ "Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
1846
+ );
1847
+ }
1848
+ const normalizedKey = privateKey.replace(/\\n/g, "\n");
1849
+ const auth = new GoogleAuth2({
1850
+ credentials: { client_email: clientEmail, private_key: normalizedKey },
1851
+ scopes: ["https://www.googleapis.com/auth/drive.readonly"]
1852
+ });
1853
+ const client = await auth.getClient();
1854
+ const tokenResponse = await client.getAccessToken();
1855
+ return tokenResponse.token;
1856
+ }
1857
+ async function listFilesInFolder3(folderId, mimeType, token) {
1858
+ const results = [];
1859
+ let pageToken;
1860
+ do {
1861
+ const query = `'${folderId}' in parents and mimeType = '${mimeType}' and trashed = false`;
1862
+ const params = new URLSearchParams({
1863
+ q: query,
1864
+ fields: "nextPageToken,files(id,name,mimeType,modifiedTime)",
1865
+ pageSize: "1000"
1866
+ });
1867
+ if (pageToken) params.set("pageToken", pageToken);
1868
+ const response = await fetch(`${DRIVE_FILES_URL3}?${params.toString()}`, {
1869
+ headers: { Authorization: `Bearer ${token}` }
1870
+ });
1871
+ if (!response.ok) {
1872
+ const text = await response.text();
1873
+ throw new Error(`Drive API error ${response.status}: ${text}`);
1874
+ }
1875
+ const data = await response.json();
1876
+ results.push(...data.files);
1877
+ pageToken = data.nextPageToken;
1878
+ } while (pageToken);
1879
+ return results;
1880
+ }
1881
+ function inferLocaleFromDocName(name) {
1882
+ const baseName = name.replace(/\.[^.]+$/, "");
1883
+ const match = baseName.match(/_([a-zA-Z]{2,3}(?:[-_][a-zA-Z]{2,4})?)$/);
1884
+ if (!match) return void 0;
1885
+ const candidate = match[1].replace("_", "-");
1886
+ const parts = candidate.split("-");
1887
+ if (parts.length === 2) {
1888
+ return `${parts[0].toLowerCase()}-${parts[1].toUpperCase()}`;
1889
+ }
1890
+ return parts[0].toLowerCase();
1891
+ }
1892
+ async function scanFolder2(folderId, folderPath, token, recursive, nameFilter, seen = /* @__PURE__ */ new Set()) {
1893
+ console.log(
1894
+ `[driveDocScanner] Scanning folder: ${folderId} (path: "${folderPath}")`
1895
+ );
1896
+ const docs = await listFilesInFolder3(folderId, DOC_MIME, token);
1897
+ const results = [];
1898
+ for (const file of docs) {
1899
+ if (seen.has(file.id)) continue;
1900
+ seen.add(file.id);
1901
+ if (nameFilter && !nameFilter.test(file.name)) continue;
1902
+ results.push({
1903
+ id: file.id,
1904
+ name: file.name,
1905
+ folderPath,
1906
+ mimeType: file.mimeType,
1907
+ modifiedTime: file.modifiedTime,
1908
+ sourceLocale: inferLocaleFromDocName(file.name)
1909
+ });
1910
+ }
1911
+ if (recursive) {
1912
+ const subfolders = await listFilesInFolder3(folderId, FOLDER_MIME3, token);
1913
+ for (const folder of subfolders) {
1914
+ const subPath = folderPath ? `${folderPath}/${folder.name}` : folder.name;
1915
+ const subResults = await scanFolder2(
1916
+ folder.id,
1917
+ subPath,
1918
+ token,
1919
+ recursive,
1920
+ nameFilter,
1921
+ seen
1922
+ );
1923
+ results.push(...subResults);
1924
+ }
1925
+ }
1926
+ return results;
1927
+ }
1928
+ async function scanDriveFolderForDocs(options) {
1929
+ const { folderId, recursive = true, nameFilter, credentials } = options;
1930
+ const token = await getAccessToken3(credentials);
1931
+ return scanFolder2(folderId, "", token, recursive, nameFilter);
1932
+ }
1933
+
1934
+ // src/utils/docIngester.ts
1935
+ import { GoogleAuth as GoogleAuth3 } from "google-auth-library";
1936
+ import { GoogleSpreadsheet as GoogleSpreadsheet3 } from "google-spreadsheet";
1937
+
1938
+ // src/utils/docParser.ts
1939
+ function slugifyKey(text) {
1940
+ return text.toLowerCase().replace(/[^\w]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1941
+ }
1942
+ function parseDocContent(content, options = {}) {
1943
+ const { strategy = "heading", defaultSheetName = "content" } = options;
1944
+ if (strategy === "marker") return parseWithMarkers(content, defaultSheetName);
1945
+ if (strategy === "numbered") return parseNumbered(content, defaultSheetName);
1946
+ return parseWithHeadings(content, defaultSheetName);
1947
+ }
1948
+ function parseWithHeadings(content, defaultSheetName) {
1949
+ const lines = content.split("\n");
1950
+ const entries = [];
1951
+ let currentSheet = defaultSheetName;
1952
+ let currentKey = null;
1953
+ const valueLines = [];
1954
+ function flushEntry() {
1955
+ if (currentKey !== null) {
1956
+ const value = valueLines.join("\n").trim();
1957
+ if (value) {
1958
+ entries.push({ sheetName: currentSheet, key: currentKey, value });
1959
+ }
1960
+ currentKey = null;
1961
+ valueLines.length = 0;
1962
+ }
1963
+ }
1964
+ for (const rawLine of lines) {
1965
+ const line = rawLine.trimEnd();
1966
+ if (line.startsWith("# ")) {
1967
+ flushEntry();
1968
+ currentSheet = slugifyKey(line.slice(2).trim()) || defaultSheetName;
1969
+ currentKey = null;
1970
+ valueLines.length = 0;
1971
+ } else if (line.startsWith("## ")) {
1972
+ flushEntry();
1973
+ currentKey = slugifyKey(line.slice(3).trim());
1974
+ valueLines.length = 0;
1975
+ } else if (currentKey !== null) {
1976
+ valueLines.push(line);
1977
+ }
1978
+ }
1979
+ flushEntry();
1980
+ return entries;
1981
+ }
1982
+ function parseWithMarkers(content, defaultSheetName) {
1983
+ const MARKER_RE = /\[\[key:([^\]]{1,200})\]\]/g;
1984
+ const entries = [];
1985
+ const segments = content.split(MARKER_RE);
1986
+ for (let i = 1; i < segments.length; i += 2) {
1987
+ const keyPath = segments[i].trim();
1988
+ const value = (segments[i + 1] ?? "").trim();
1989
+ if (!keyPath || !value) continue;
1990
+ const dotIdx = keyPath.indexOf(".");
1991
+ let sheetName;
1992
+ let key;
1993
+ if (dotIdx !== -1) {
1994
+ sheetName = slugifyKey(keyPath.slice(0, dotIdx));
1995
+ key = slugifyKey(keyPath.slice(dotIdx + 1));
1996
+ } else {
1997
+ sheetName = defaultSheetName;
1998
+ key = slugifyKey(keyPath);
1999
+ }
2000
+ if (sheetName && key) {
2001
+ entries.push({ sheetName, key, value });
2002
+ }
2003
+ }
2004
+ return entries;
2005
+ }
2006
+ function parseNumbered(content, defaultSheetName) {
2007
+ const entries = [];
2008
+ let counter = 0;
2009
+ const paragraphs = content.split(/\n{2,}/);
2010
+ for (const para of paragraphs) {
2011
+ const value = para.replace(/^[#\s]+/, "").trim();
2012
+ if (value) {
2013
+ counter++;
2014
+ entries.push({
2015
+ sheetName: defaultSheetName,
2016
+ key: `item_${counter}`,
2017
+ value
2018
+ });
2019
+ }
2020
+ }
2021
+ return entries;
2022
+ }
2023
+
2024
+ // src/utils/docIngester.ts
2025
+ async function getDriveExportToken(credentials) {
2026
+ const clientEmail = credentials?.GOOGLE_CLIENT_EMAIL ?? process.env.GOOGLE_CLIENT_EMAIL;
2027
+ const privateKey = credentials?.GOOGLE_PRIVATE_KEY ?? process.env.GOOGLE_PRIVATE_KEY;
2028
+ if (!clientEmail || !privateKey) {
2029
+ throw new Error(
2030
+ "Google Drive credentials required: GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY"
2031
+ );
2032
+ }
2033
+ const normalizedKey = privateKey.replace(/\\n/g, "\n");
2034
+ const auth = new GoogleAuth3({
2035
+ credentials: { client_email: clientEmail, private_key: normalizedKey },
2036
+ scopes: ["https://www.googleapis.com/auth/drive.readonly"]
2037
+ });
2038
+ const client = await auth.getClient();
2039
+ const tokenResponse = await client.getAccessToken();
2040
+ return tokenResponse.token;
2041
+ }
2042
+ async function exportDoc(docId, credentials) {
2043
+ const token = await getDriveExportToken(credentials);
2044
+ const base = `https://www.googleapis.com/drive/v3/files/${docId}/export`;
2045
+ const mdRes = await fetch(
2046
+ `${base}?mimeType=text%2Fmarkdown`,
2047
+ { headers: { Authorization: `Bearer ${token}` } }
2048
+ );
2049
+ if (mdRes.ok) return mdRes.text();
2050
+ const txtRes = await fetch(
2051
+ `${base}?mimeType=text%2Fplain`,
2052
+ { headers: { Authorization: `Bearer ${token}` } }
2053
+ );
2054
+ if (txtRes.ok) return txtRes.text();
2055
+ const errText = await txtRes.text();
2056
+ throw new Error(
2057
+ `Failed to export doc ${docId}: HTTP ${txtRes.status} \u2013 ${errText}`
2058
+ );
2059
+ }
2060
+ function entriesToSeedKeys(entries) {
2061
+ const keys = {};
2062
+ const counts = /* @__PURE__ */ new Map();
2063
+ for (const entry of entries) {
2064
+ const base = `${entry.sheetName}.${entry.key}`;
2065
+ const count = (counts.get(base) ?? 0) + 1;
2066
+ counts.set(base, count);
2067
+ const finalKey = count > 1 ? `${base}_${count}` : base;
2068
+ keys[finalKey] = entry.value;
2069
+ }
2070
+ return keys;
2071
+ }
2072
+ function entriesToTranslationData(entries, locale) {
2073
+ const data = {};
2074
+ data[locale] = {};
2075
+ const counts = /* @__PURE__ */ new Map();
2076
+ for (const entry of entries) {
2077
+ const sheetName = entry.sheetName;
2078
+ const entryKey = entry.key;
2079
+ if (sheetName === "__proto__" || sheetName === "constructor" || sheetName === "prototype" || entryKey === "__proto__" || entryKey === "constructor" || entryKey === "prototype") {
2080
+ continue;
2081
+ }
2082
+ if (!data[locale][sheetName]) {
2083
+ data[locale][sheetName] = {};
2084
+ }
2085
+ const base = `${sheetName}::${entryKey}`;
2086
+ const count = (counts.get(base) ?? 0) + 1;
2087
+ counts.set(base, count);
2088
+ const finalKey = count > 1 ? `${entryKey}_${count}` : entryKey;
2089
+ data[locale][sheetName][finalKey] = entry.value;
2090
+ }
2091
+ return data;
2092
+ }
2093
+ function docTitle(name) {
2094
+ return name.replace(/_[a-zA-Z]{2,3}(?:[-_][a-zA-Z]{2,4})?$/, "").trim() || name;
2095
+ }
2096
+ async function ingestDoc(docFile, options = {}) {
2097
+ const {
2098
+ targetLocales,
2099
+ keyStrategy = "heading",
2100
+ updateMode = "create-only",
2101
+ credentials,
2102
+ existingEntry,
2103
+ waitSeconds = 1
2104
+ } = options;
2105
+ const sourceLocale = docFile.sourceLocale ?? "en";
2106
+ const entry = existingEntry ? { ...existingEntry, modifiedTime: docFile.modifiedTime } : {
2107
+ id: docFile.id,
2108
+ name: docFile.name,
2109
+ folderPath: docFile.folderPath,
2110
+ generatedFromDoc: true,
2111
+ sourceLocale,
2112
+ modifiedTime: docFile.modifiedTime
2113
+ };
2114
+ const hasLinkedSheet = !!entry.linkedSpreadsheetId;
2115
+ const shouldRefresh = updateMode === "refresh-if-newer" && hasLinkedSheet && !!docFile.modifiedTime && !!existingEntry?.lastIngestedAt && new Date(docFile.modifiedTime) > new Date(existingEntry.lastIngestedAt);
2116
+ if (hasLinkedSheet && !shouldRefresh) {
2117
+ console.log(
2118
+ `[docIngester] Skipping "${docFile.name}" \u2013 linked spreadsheet is already up-to-date.`
2119
+ );
2120
+ return { action: "skipped", entry };
2121
+ }
2122
+ console.log(
2123
+ `[docIngester] Exporting doc "${docFile.name}" (id: ${docFile.id})\u2026`
2124
+ );
2125
+ const content = await exportDoc(docFile.id, credentials);
2126
+ const sheetBaseName = slugifyKey(docTitle(docFile.name)) || "content";
2127
+ const entries = parseDocContent(content, {
2128
+ strategy: keyStrategy,
2129
+ defaultSheetName: sheetBaseName
2130
+ });
2131
+ if (entries.length === 0) {
2132
+ console.warn(
2133
+ `[docIngester] Doc "${docFile.name}" produced no translation entries \u2013 skipping.`
2134
+ );
2135
+ return { action: "skipped", entry };
2136
+ }
2137
+ if (!hasLinkedSheet) {
2138
+ const authClient2 = createAuthClient();
2139
+ const seedKeys = entriesToSeedKeys(entries);
2140
+ const title = docTitle(docFile.name);
2141
+ const { spreadsheetId: spreadsheetId2 } = await createSpreadsheet(authClient2, {
2142
+ title,
2143
+ sourceLocale,
2144
+ targetLocales,
2145
+ seedKeys
2146
+ });
2147
+ entry.linkedSpreadsheetId = spreadsheetId2;
2148
+ entry.lastIngestedAt = (/* @__PURE__ */ new Date()).toISOString();
2149
+ console.log(
2150
+ `[docIngester] Created spreadsheet ${spreadsheetId2} from doc "${docFile.name}".`
2151
+ );
2152
+ return { action: "created", entry };
2153
+ }
2154
+ const authClient = createAuthClient();
2155
+ const spreadsheetId = entry.linkedSpreadsheetId;
2156
+ const doc = new GoogleSpreadsheet3(spreadsheetId, authClient);
2157
+ await doc.loadInfo();
2158
+ const changes = entriesToTranslationData(entries, sourceLocale);
2159
+ await updateSpreadsheetWithLocalChanges(
2160
+ doc,
2161
+ changes,
2162
+ waitSeconds,
2163
+ false,
2164
+ // autoTranslate – formulas already exist in non-base columns
2165
+ {},
2166
+ false
2167
+ );
2168
+ entry.lastIngestedAt = (/* @__PURE__ */ new Date()).toISOString();
2169
+ console.log(
2170
+ `[docIngester] Refreshed spreadsheet ${spreadsheetId} from doc "${docFile.name}".`
2171
+ );
2172
+ return { action: "refreshed", entry };
2173
+ }
1787
2174
 
1788
2175
  // src/utils/getDriveTranslations.ts
1789
2176
  function sanitizeFolderName(name) {
1790
2177
  return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "sheet";
1791
2178
  }
2179
+ async function moveSpreadsheetToFolder(spreadsheetId, folderId) {
2180
+ const clientEmail = process.env.GOOGLE_CLIENT_EMAIL;
2181
+ const rawPrivateKey = process.env.GOOGLE_PRIVATE_KEY;
2182
+ let credentials;
2183
+ if (clientEmail && rawPrivateKey) {
2184
+ credentials = { client_email: clientEmail, private_key: normalizePrivateKey(rawPrivateKey) };
2185
+ }
2186
+ const driveAuth = buildGoogleAuth(
2187
+ ["https://www.googleapis.com/auth/drive.file"],
2188
+ credentials
2189
+ );
2190
+ const fileRes = await driveAuth.request({
2191
+ url: `https://www.googleapis.com/drive/v3/files/${spreadsheetId}`,
2192
+ params: { fields: "parents" }
2193
+ });
2194
+ const parentIds = fileRes.data.parents ?? [];
2195
+ await driveAuth.request({
2196
+ url: `https://www.googleapis.com/drive/v3/files/${spreadsheetId}`,
2197
+ method: "PATCH",
2198
+ params: {
2199
+ addParents: folderId,
2200
+ ...parentIds.length > 0 ? { removeParents: parentIds.join(",") } : {},
2201
+ fields: "id,parents"
2202
+ }
2203
+ });
2204
+ }
1792
2205
  async function manageDriveTranslations(options) {
1793
2206
  const {
1794
2207
  driveFolderId,
@@ -1806,7 +2219,14 @@ async function manageDriveTranslations(options) {
1806
2219
  projectName,
1807
2220
  domain,
1808
2221
  defaultLocale,
1809
- projectMetadata
2222
+ projectMetadata,
2223
+ // Doc ingestion options
2224
+ scanForDocs = false,
2225
+ docNameFilter,
2226
+ docSourceLocale,
2227
+ docKeyStrategy,
2228
+ docUpdateMode,
2229
+ docTargetLocales
1810
2230
  } = options;
1811
2231
  if (syncImages && !imageOutputPath) {
1812
2232
  throw new Error(
@@ -1837,6 +2257,36 @@ async function manageDriveTranslations(options) {
1837
2257
  if (!name) return true;
1838
2258
  return spreadsheetNameFilter.test(name);
1839
2259
  }) : allIds;
2260
+ if (driveFolderId && filteredIds.length === 0 && translationOptions.autoCreate !== false) {
2261
+ console.log(
2262
+ `[manageDriveTranslations] Drive folder "${driveFolderId}" contains no spreadsheets. Bootstrapping a new spreadsheet\u2026`
2263
+ );
2264
+ const authClient = createAuthClient();
2265
+ const bootstrapTitle = translationOptions.spreadsheetTitle ?? "google-sheet-translations";
2266
+ const created = await createSpreadsheet(authClient, {
2267
+ title: bootstrapTitle,
2268
+ sourceLocale: translationOptions.sourceLocale,
2269
+ targetLocales: translationOptions.targetLocales
2270
+ });
2271
+ console.log(`[manageDriveTranslations] \u2705 Spreadsheet created: ${created.url}`);
2272
+ try {
2273
+ await moveSpreadsheetToFolder(created.spreadsheetId, driveFolderId);
2274
+ console.log(
2275
+ `[manageDriveTranslations] \u2705 Spreadsheet moved into Drive folder "${driveFolderId}"`
2276
+ );
2277
+ } catch (moveErr) {
2278
+ console.warn(
2279
+ `[manageDriveTranslations] \u26A0\uFE0F Could not move spreadsheet into Drive folder:`,
2280
+ moveErr.message
2281
+ );
2282
+ console.warn(
2283
+ ` Please move spreadsheet "${created.spreadsheetId}" into folder "${driveFolderId}" manually.`
2284
+ );
2285
+ }
2286
+ filteredIds.push(created.spreadsheetId);
2287
+ discoveredNames.set(created.spreadsheetId, bootstrapTitle);
2288
+ discoveredFolderPaths.set(created.spreadsheetId, "");
2289
+ }
1840
2290
  let translations;
1841
2291
  const spreadsheetEntries = [];
1842
2292
  const baseOutputDir = translationOptions.translationsOutputDir ?? "translations";
@@ -1890,8 +2340,48 @@ async function manageDriveTranslations(options) {
1890
2340
  });
1891
2341
  }
1892
2342
  let manifest;
2343
+ let docIngestResults;
2344
+ const docEntries = [];
2345
+ const resolvedManifestPath = manifestPath ?? path6.join(baseOutputDir, "i18n-manifest.json");
2346
+ if (driveFolderId && scanForDocs) {
2347
+ const previousManifest = readManifest(resolvedManifestPath);
2348
+ const docScanOptions = {
2349
+ folderId: driveFolderId
2350
+ };
2351
+ const discoveredDocs = await scanDriveFolderForDocs(docScanOptions);
2352
+ console.log(
2353
+ `[manageDriveTranslations] Found ${discoveredDocs.length} doc(s) in Drive folder`
2354
+ );
2355
+ docIngestResults = [];
2356
+ for (const docFile of discoveredDocs) {
2357
+ if (docNameFilter && !docNameFilter.test(docFile.name)) continue;
2358
+ if (!docFile.sourceLocale && docSourceLocale) {
2359
+ docFile.sourceLocale = docSourceLocale;
2360
+ }
2361
+ const existingEntry = previousManifest?.docs?.find(
2362
+ (d) => d.id === docFile.id
2363
+ );
2364
+ const ingesterOptions = {
2365
+ targetLocales: docTargetLocales,
2366
+ keyStrategy: docKeyStrategy,
2367
+ updateMode: docUpdateMode,
2368
+ existingEntry,
2369
+ waitSeconds: translationOptions.waitSeconds
2370
+ };
2371
+ try {
2372
+ const result = await ingestDoc(docFile, ingesterOptions);
2373
+ docEntries.push(result.entry);
2374
+ docIngestResults.push({ docName: docFile.name, action: result.action });
2375
+ } catch (err) {
2376
+ console.error(
2377
+ `[manageDriveTranslations] Failed to ingest doc "${docFile.name}":`,
2378
+ err
2379
+ );
2380
+ if (existingEntry) docEntries.push(existingEntry);
2381
+ }
2382
+ }
2383
+ }
1893
2384
  if (shouldCreateManifest) {
1894
- const resolvedManifestPath = manifestPath ?? path6.join(baseOutputDir, "i18n-manifest.json");
1895
2385
  manifest = buildManifest({
1896
2386
  translations,
1897
2387
  spreadsheets: spreadsheetEntries,
@@ -1900,11 +2390,12 @@ async function manageDriveTranslations(options) {
1900
2390
  projectName,
1901
2391
  domain,
1902
2392
  defaultLocale,
1903
- projectMetadata
2393
+ projectMetadata,
2394
+ docs: docEntries.length > 0 ? docEntries : void 0
1904
2395
  });
1905
2396
  writeManifest(manifest, resolvedManifestPath);
1906
2397
  }
1907
- return { translations, spreadsheetIds: filteredIds, imageSync, manifest };
2398
+ return { translations, spreadsheetIds: filteredIds, imageSync, manifest, docIngestResults };
1908
2399
  }
1909
2400
 
1910
2401
  // src/index.ts
@@ -1912,6 +2403,7 @@ var index_default = getSpreadSheetData;
1912
2403
  export {
1913
2404
  DEFAULT_IMAGE_EXTENSIONS,
1914
2405
  DEFAULT_WAIT_SECONDS,
2406
+ buildGoogleAuth,
1915
2407
  buildManifest,
1916
2408
  convertFromDataJsonFormat,
1917
2409
  convertToDataJsonFormat,
@@ -1919,6 +2411,9 @@ export {
1919
2411
  createLocaleMapping,
1920
2412
  createSpreadsheet,
1921
2413
  index_default as default,
2414
+ entriesToSeedKeys,
2415
+ entriesToTranslationData,
2416
+ exportDoc,
1922
2417
  filterValidLocales,
1923
2418
  findLocalChanges,
1924
2419
  getGoogleTranslateCode,
@@ -1930,16 +2425,23 @@ export {
1930
2425
  getSpreadSheetData,
1931
2426
  getTranslationSummary,
1932
2427
  handleBidirectionalSync,
2428
+ inferLocaleFromDocName,
2429
+ ingestDoc,
1933
2430
  isValidLocale,
1934
2431
  manageDriveTranslations,
1935
2432
  mergeMultipleTranslationData,
1936
2433
  mergeSheets,
1937
2434
  normalizeExtension,
1938
2435
  normalizeLocaleCode,
2436
+ normalizePrivateKey,
2437
+ parseDocContent,
1939
2438
  processRawRows,
2439
+ readManifest,
1940
2440
  readPublicSheet,
1941
2441
  resolveLocaleWithFallback,
2442
+ scanDriveFolderForDocs,
1942
2443
  scanDriveFolderForSpreadsheets,
2444
+ slugifyKey,
1943
2445
  syncDriveImages,
1944
2446
  updateSpreadsheetWithLocalChanges,
1945
2447
  validateCredentials,