@el-j/google-sheet-translations 2.2.0-beta.3 → 2.2.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/action-entrypoint.d.ts.map +1 -1
- package/dist/esm/index.js +694 -12
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +706 -12
- package/dist/setup/cli.d.ts +13 -0
- package/dist/setup/cli.d.ts.map +1 -0
- package/dist/setup/wifSetup.d.ts +95 -0
- package/dist/setup/wifSetup.d.ts.map +1 -0
- 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/dist-cli/setup-wif.mjs +18948 -0
- package/package.json +25 -20
package/dist/esm/index.js
CHANGED
|
@@ -1188,13 +1188,13 @@ GOOGLE_SPREADSHEET_ID=${id}
|
|
|
1188
1188
|
async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
|
|
1189
1189
|
const config = normalizeConfig(options);
|
|
1190
1190
|
const baseDelayMs = config.waitSeconds * 1e3;
|
|
1191
|
-
const
|
|
1192
|
-
if (
|
|
1191
|
+
const docTitle2 = _docTitle ?? [];
|
|
1192
|
+
if (docTitle2.length === 0) {
|
|
1193
1193
|
console.warn("No sheet titles provided, cannot process spreadsheet data");
|
|
1194
1194
|
return {};
|
|
1195
1195
|
}
|
|
1196
|
-
if (!
|
|
1197
|
-
|
|
1196
|
+
if (!docTitle2.includes("i18n")) {
|
|
1197
|
+
docTitle2.push("i18n");
|
|
1198
1198
|
}
|
|
1199
1199
|
const finalTranslations = {};
|
|
1200
1200
|
const allLocales = /* @__PURE__ */ new Set();
|
|
@@ -1231,9 +1231,9 @@ async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
|
|
|
1231
1231
|
"No spreadsheet ID provided. Set GOOGLE_SPREADSHEET_ID or pass spreadsheetId in options."
|
|
1232
1232
|
);
|
|
1233
1233
|
}
|
|
1234
|
-
console.log(`Processing ${
|
|
1234
|
+
console.log(`Processing ${docTitle2.length} sheets: ${docTitle2.join(", ")}`);
|
|
1235
1235
|
await Promise.all(
|
|
1236
|
-
|
|
1236
|
+
docTitle2.map(async (title) => {
|
|
1237
1237
|
let rows;
|
|
1238
1238
|
try {
|
|
1239
1239
|
rows = await withRetry(
|
|
@@ -1266,7 +1266,7 @@ async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
|
|
|
1266
1266
|
);
|
|
1267
1267
|
}
|
|
1268
1268
|
}
|
|
1269
|
-
console.log(`Processing ${
|
|
1269
|
+
console.log(`Processing ${docTitle2.length} sheets: ${docTitle2.join(", ")}`);
|
|
1270
1270
|
const doc = new GoogleSpreadsheet2(spreadsheetId, serviceAuthClient);
|
|
1271
1271
|
try {
|
|
1272
1272
|
await withRetry(() => doc.loadInfo(true), "loadInfo", baseDelayMs);
|
|
@@ -1274,7 +1274,7 @@ async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
|
|
|
1274
1274
|
throw new Error(`Failed to load spreadsheet "${spreadsheetId}"`, { cause: err });
|
|
1275
1275
|
}
|
|
1276
1276
|
await Promise.all(
|
|
1277
|
-
|
|
1277
|
+
docTitle2.map(async (title) => {
|
|
1278
1278
|
const sheet = doc.sheetsByTitle[title];
|
|
1279
1279
|
if (!sheet) {
|
|
1280
1280
|
console.warn(`Sheet "${title}" not found in the document`);
|
|
@@ -1809,6 +1809,7 @@ function buildManifest(options) {
|
|
|
1809
1809
|
locales,
|
|
1810
1810
|
defaultLocale: options.defaultLocale,
|
|
1811
1811
|
spreadsheets: options.spreadsheets,
|
|
1812
|
+
docs: options.docs,
|
|
1812
1813
|
outputDirectory: options.outputDirectory,
|
|
1813
1814
|
flatten: options.flatten,
|
|
1814
1815
|
projectMetadata: options.projectMetadata
|
|
@@ -1822,11 +1823,388 @@ function writeManifest(manifest, manifestPath) {
|
|
|
1822
1823
|
fs6.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
1823
1824
|
console.log(`[driveProjectIndex] Wrote project manifest \u2192 ${manifestPath}`);
|
|
1824
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
|
+
if (locale === "__proto__" || locale === "constructor" || locale === "prototype") {
|
|
2074
|
+
return {};
|
|
2075
|
+
}
|
|
2076
|
+
const data = {};
|
|
2077
|
+
data[locale] = {};
|
|
2078
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2079
|
+
for (const entry of entries) {
|
|
2080
|
+
const sheetName = entry.sheetName;
|
|
2081
|
+
const entryKey = entry.key;
|
|
2082
|
+
if (sheetName === "__proto__" || sheetName === "constructor" || sheetName === "prototype" || entryKey === "__proto__" || entryKey === "constructor" || entryKey === "prototype") {
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
if (!data[locale][sheetName]) {
|
|
2086
|
+
data[locale][sheetName] = {};
|
|
2087
|
+
}
|
|
2088
|
+
const base = `${sheetName}::${entryKey}`;
|
|
2089
|
+
const count = (counts.get(base) ?? 0) + 1;
|
|
2090
|
+
counts.set(base, count);
|
|
2091
|
+
const finalKey = count > 1 ? `${entryKey}_${count}` : entryKey;
|
|
2092
|
+
data[locale][sheetName][finalKey] = entry.value;
|
|
2093
|
+
}
|
|
2094
|
+
return data;
|
|
2095
|
+
}
|
|
2096
|
+
function docTitle(name) {
|
|
2097
|
+
return name.replace(/_[a-zA-Z]{2,3}(?:[-_][a-zA-Z]{2,4})?$/, "").trim() || name;
|
|
2098
|
+
}
|
|
2099
|
+
async function ingestDoc(docFile, options = {}) {
|
|
2100
|
+
const {
|
|
2101
|
+
targetLocales,
|
|
2102
|
+
keyStrategy = "heading",
|
|
2103
|
+
updateMode = "create-only",
|
|
2104
|
+
credentials,
|
|
2105
|
+
existingEntry,
|
|
2106
|
+
waitSeconds = 1
|
|
2107
|
+
} = options;
|
|
2108
|
+
const sourceLocale = docFile.sourceLocale ?? "en";
|
|
2109
|
+
const entry = existingEntry ? { ...existingEntry, modifiedTime: docFile.modifiedTime } : {
|
|
2110
|
+
id: docFile.id,
|
|
2111
|
+
name: docFile.name,
|
|
2112
|
+
folderPath: docFile.folderPath,
|
|
2113
|
+
generatedFromDoc: true,
|
|
2114
|
+
sourceLocale,
|
|
2115
|
+
modifiedTime: docFile.modifiedTime
|
|
2116
|
+
};
|
|
2117
|
+
const hasLinkedSheet = !!entry.linkedSpreadsheetId;
|
|
2118
|
+
const shouldRefresh = updateMode === "refresh-if-newer" && hasLinkedSheet && !!docFile.modifiedTime && !!existingEntry?.lastIngestedAt && new Date(docFile.modifiedTime) > new Date(existingEntry.lastIngestedAt);
|
|
2119
|
+
if (hasLinkedSheet && !shouldRefresh) {
|
|
2120
|
+
console.log(
|
|
2121
|
+
`[docIngester] Skipping "${docFile.name}" \u2013 linked spreadsheet is already up-to-date.`
|
|
2122
|
+
);
|
|
2123
|
+
return { action: "skipped", entry };
|
|
2124
|
+
}
|
|
2125
|
+
console.log(
|
|
2126
|
+
`[docIngester] Exporting doc "${docFile.name}" (id: ${docFile.id})\u2026`
|
|
2127
|
+
);
|
|
2128
|
+
const content = await exportDoc(docFile.id, credentials);
|
|
2129
|
+
const sheetBaseName = slugifyKey(docTitle(docFile.name)) || "content";
|
|
2130
|
+
const entries = parseDocContent(content, {
|
|
2131
|
+
strategy: keyStrategy,
|
|
2132
|
+
defaultSheetName: sheetBaseName
|
|
2133
|
+
});
|
|
2134
|
+
if (entries.length === 0) {
|
|
2135
|
+
console.warn(
|
|
2136
|
+
`[docIngester] Doc "${docFile.name}" produced no translation entries \u2013 skipping.`
|
|
2137
|
+
);
|
|
2138
|
+
return { action: "skipped", entry };
|
|
2139
|
+
}
|
|
2140
|
+
if (!hasLinkedSheet) {
|
|
2141
|
+
const authClient2 = createAuthClient();
|
|
2142
|
+
const seedKeys = entriesToSeedKeys(entries);
|
|
2143
|
+
const title = docTitle(docFile.name);
|
|
2144
|
+
const { spreadsheetId: spreadsheetId2 } = await createSpreadsheet(authClient2, {
|
|
2145
|
+
title,
|
|
2146
|
+
sourceLocale,
|
|
2147
|
+
targetLocales,
|
|
2148
|
+
seedKeys
|
|
2149
|
+
});
|
|
2150
|
+
entry.linkedSpreadsheetId = spreadsheetId2;
|
|
2151
|
+
entry.lastIngestedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2152
|
+
console.log(
|
|
2153
|
+
`[docIngester] Created spreadsheet ${spreadsheetId2} from doc "${docFile.name}".`
|
|
2154
|
+
);
|
|
2155
|
+
return { action: "created", entry };
|
|
2156
|
+
}
|
|
2157
|
+
const authClient = createAuthClient();
|
|
2158
|
+
const spreadsheetId = entry.linkedSpreadsheetId;
|
|
2159
|
+
const doc = new GoogleSpreadsheet3(spreadsheetId, authClient);
|
|
2160
|
+
await doc.loadInfo();
|
|
2161
|
+
const changes = entriesToTranslationData(entries, sourceLocale);
|
|
2162
|
+
await updateSpreadsheetWithLocalChanges(
|
|
2163
|
+
doc,
|
|
2164
|
+
changes,
|
|
2165
|
+
waitSeconds,
|
|
2166
|
+
false,
|
|
2167
|
+
// autoTranslate – formulas already exist in non-base columns
|
|
2168
|
+
{},
|
|
2169
|
+
false
|
|
2170
|
+
);
|
|
2171
|
+
entry.lastIngestedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2172
|
+
console.log(
|
|
2173
|
+
`[docIngester] Refreshed spreadsheet ${spreadsheetId} from doc "${docFile.name}".`
|
|
2174
|
+
);
|
|
2175
|
+
return { action: "refreshed", entry };
|
|
2176
|
+
}
|
|
1825
2177
|
|
|
1826
2178
|
// src/utils/getDriveTranslations.ts
|
|
1827
2179
|
function sanitizeFolderName(name) {
|
|
1828
2180
|
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "sheet";
|
|
1829
2181
|
}
|
|
2182
|
+
async function moveSpreadsheetToFolder(spreadsheetId, folderId) {
|
|
2183
|
+
const clientEmail = process.env.GOOGLE_CLIENT_EMAIL;
|
|
2184
|
+
const rawPrivateKey = process.env.GOOGLE_PRIVATE_KEY;
|
|
2185
|
+
let credentials;
|
|
2186
|
+
if (clientEmail && rawPrivateKey) {
|
|
2187
|
+
credentials = { client_email: clientEmail, private_key: normalizePrivateKey(rawPrivateKey) };
|
|
2188
|
+
}
|
|
2189
|
+
const driveAuth = buildGoogleAuth(
|
|
2190
|
+
["https://www.googleapis.com/auth/drive.file"],
|
|
2191
|
+
credentials
|
|
2192
|
+
);
|
|
2193
|
+
const fileRes = await driveAuth.request({
|
|
2194
|
+
url: `https://www.googleapis.com/drive/v3/files/${spreadsheetId}`,
|
|
2195
|
+
params: { fields: "parents" }
|
|
2196
|
+
});
|
|
2197
|
+
const parentIds = fileRes.data.parents ?? [];
|
|
2198
|
+
await driveAuth.request({
|
|
2199
|
+
url: `https://www.googleapis.com/drive/v3/files/${spreadsheetId}`,
|
|
2200
|
+
method: "PATCH",
|
|
2201
|
+
params: {
|
|
2202
|
+
addParents: folderId,
|
|
2203
|
+
...parentIds.length > 0 ? { removeParents: parentIds.join(",") } : {},
|
|
2204
|
+
fields: "id,parents"
|
|
2205
|
+
}
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
1830
2208
|
async function manageDriveTranslations(options) {
|
|
1831
2209
|
const {
|
|
1832
2210
|
driveFolderId,
|
|
@@ -1844,7 +2222,14 @@ async function manageDriveTranslations(options) {
|
|
|
1844
2222
|
projectName,
|
|
1845
2223
|
domain,
|
|
1846
2224
|
defaultLocale,
|
|
1847
|
-
projectMetadata
|
|
2225
|
+
projectMetadata,
|
|
2226
|
+
// Doc ingestion options
|
|
2227
|
+
scanForDocs = false,
|
|
2228
|
+
docNameFilter,
|
|
2229
|
+
docSourceLocale,
|
|
2230
|
+
docKeyStrategy,
|
|
2231
|
+
docUpdateMode,
|
|
2232
|
+
docTargetLocales
|
|
1848
2233
|
} = options;
|
|
1849
2234
|
if (syncImages && !imageOutputPath) {
|
|
1850
2235
|
throw new Error(
|
|
@@ -1875,6 +2260,36 @@ async function manageDriveTranslations(options) {
|
|
|
1875
2260
|
if (!name) return true;
|
|
1876
2261
|
return spreadsheetNameFilter.test(name);
|
|
1877
2262
|
}) : allIds;
|
|
2263
|
+
if (driveFolderId && filteredIds.length === 0 && translationOptions.autoCreate !== false) {
|
|
2264
|
+
console.log(
|
|
2265
|
+
`[manageDriveTranslations] Drive folder "${driveFolderId}" contains no spreadsheets. Bootstrapping a new spreadsheet\u2026`
|
|
2266
|
+
);
|
|
2267
|
+
const authClient = createAuthClient();
|
|
2268
|
+
const bootstrapTitle = translationOptions.spreadsheetTitle ?? "google-sheet-translations";
|
|
2269
|
+
const created = await createSpreadsheet(authClient, {
|
|
2270
|
+
title: bootstrapTitle,
|
|
2271
|
+
sourceLocale: translationOptions.sourceLocale,
|
|
2272
|
+
targetLocales: translationOptions.targetLocales
|
|
2273
|
+
});
|
|
2274
|
+
console.log(`[manageDriveTranslations] \u2705 Spreadsheet created: ${created.url}`);
|
|
2275
|
+
try {
|
|
2276
|
+
await moveSpreadsheetToFolder(created.spreadsheetId, driveFolderId);
|
|
2277
|
+
console.log(
|
|
2278
|
+
`[manageDriveTranslations] \u2705 Spreadsheet moved into Drive folder "${driveFolderId}"`
|
|
2279
|
+
);
|
|
2280
|
+
} catch (moveErr) {
|
|
2281
|
+
console.warn(
|
|
2282
|
+
`[manageDriveTranslations] \u26A0\uFE0F Could not move spreadsheet into Drive folder:`,
|
|
2283
|
+
moveErr.message
|
|
2284
|
+
);
|
|
2285
|
+
console.warn(
|
|
2286
|
+
` Please move spreadsheet "${created.spreadsheetId}" into folder "${driveFolderId}" manually.`
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
filteredIds.push(created.spreadsheetId);
|
|
2290
|
+
discoveredNames.set(created.spreadsheetId, bootstrapTitle);
|
|
2291
|
+
discoveredFolderPaths.set(created.spreadsheetId, "");
|
|
2292
|
+
}
|
|
1878
2293
|
let translations;
|
|
1879
2294
|
const spreadsheetEntries = [];
|
|
1880
2295
|
const baseOutputDir = translationOptions.translationsOutputDir ?? "translations";
|
|
@@ -1928,8 +2343,48 @@ async function manageDriveTranslations(options) {
|
|
|
1928
2343
|
});
|
|
1929
2344
|
}
|
|
1930
2345
|
let manifest;
|
|
2346
|
+
let docIngestResults;
|
|
2347
|
+
const docEntries = [];
|
|
2348
|
+
const resolvedManifestPath = manifestPath ?? path6.join(baseOutputDir, "i18n-manifest.json");
|
|
2349
|
+
if (driveFolderId && scanForDocs) {
|
|
2350
|
+
const previousManifest = readManifest(resolvedManifestPath);
|
|
2351
|
+
const docScanOptions = {
|
|
2352
|
+
folderId: driveFolderId
|
|
2353
|
+
};
|
|
2354
|
+
const discoveredDocs = await scanDriveFolderForDocs(docScanOptions);
|
|
2355
|
+
console.log(
|
|
2356
|
+
`[manageDriveTranslations] Found ${discoveredDocs.length} doc(s) in Drive folder`
|
|
2357
|
+
);
|
|
2358
|
+
docIngestResults = [];
|
|
2359
|
+
for (const docFile of discoveredDocs) {
|
|
2360
|
+
if (docNameFilter && !docNameFilter.test(docFile.name)) continue;
|
|
2361
|
+
if (!docFile.sourceLocale && docSourceLocale) {
|
|
2362
|
+
docFile.sourceLocale = docSourceLocale;
|
|
2363
|
+
}
|
|
2364
|
+
const existingEntry = previousManifest?.docs?.find(
|
|
2365
|
+
(d) => d.id === docFile.id
|
|
2366
|
+
);
|
|
2367
|
+
const ingesterOptions = {
|
|
2368
|
+
targetLocales: docTargetLocales,
|
|
2369
|
+
keyStrategy: docKeyStrategy,
|
|
2370
|
+
updateMode: docUpdateMode,
|
|
2371
|
+
existingEntry,
|
|
2372
|
+
waitSeconds: translationOptions.waitSeconds
|
|
2373
|
+
};
|
|
2374
|
+
try {
|
|
2375
|
+
const result = await ingestDoc(docFile, ingesterOptions);
|
|
2376
|
+
docEntries.push(result.entry);
|
|
2377
|
+
docIngestResults.push({ docName: docFile.name, action: result.action });
|
|
2378
|
+
} catch (err) {
|
|
2379
|
+
console.error(
|
|
2380
|
+
`[manageDriveTranslations] Failed to ingest doc "${docFile.name}":`,
|
|
2381
|
+
err
|
|
2382
|
+
);
|
|
2383
|
+
if (existingEntry) docEntries.push(existingEntry);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
1931
2387
|
if (shouldCreateManifest) {
|
|
1932
|
-
const resolvedManifestPath = manifestPath ?? path6.join(baseOutputDir, "i18n-manifest.json");
|
|
1933
2388
|
manifest = buildManifest({
|
|
1934
2389
|
translations,
|
|
1935
2390
|
spreadsheets: spreadsheetEntries,
|
|
@@ -1938,11 +2393,226 @@ async function manageDriveTranslations(options) {
|
|
|
1938
2393
|
projectName,
|
|
1939
2394
|
domain,
|
|
1940
2395
|
defaultLocale,
|
|
1941
|
-
projectMetadata
|
|
2396
|
+
projectMetadata,
|
|
2397
|
+
docs: docEntries.length > 0 ? docEntries : void 0
|
|
1942
2398
|
});
|
|
1943
2399
|
writeManifest(manifest, resolvedManifestPath);
|
|
1944
2400
|
}
|
|
1945
|
-
return { translations, spreadsheetIds: filteredIds, imageSync, manifest };
|
|
2401
|
+
return { translations, spreadsheetIds: filteredIds, imageSync, manifest, docIngestResults };
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
// src/setup/wifSetup.ts
|
|
2405
|
+
import { GoogleAuth as GoogleAuth4 } from "google-auth-library";
|
|
2406
|
+
var GcpApiError = class extends Error {
|
|
2407
|
+
constructor(message, status) {
|
|
2408
|
+
super(message);
|
|
2409
|
+
this.status = status;
|
|
2410
|
+
this.name = "GcpApiError";
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
async function getAccessToken4(keyFilePath) {
|
|
2414
|
+
const auth = new GoogleAuth4({
|
|
2415
|
+
...keyFilePath ? { keyFilename: keyFilePath } : {},
|
|
2416
|
+
scopes: ["https://www.googleapis.com/auth/cloud-platform"]
|
|
2417
|
+
});
|
|
2418
|
+
const client = await auth.getClient();
|
|
2419
|
+
const tokenResponse = await client.getAccessToken();
|
|
2420
|
+
if (!tokenResponse.token) {
|
|
2421
|
+
throw new Error(
|
|
2422
|
+
"Failed to obtain a Google Cloud access token. Ensure you are authenticated via Application Default Credentials (run: gcloud auth application-default login) or provide --key-file pointing to a service account JSON key."
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2425
|
+
return tokenResponse.token;
|
|
2426
|
+
}
|
|
2427
|
+
async function gcpFetch(url, token, method = "GET", body) {
|
|
2428
|
+
const response = await fetch(url, {
|
|
2429
|
+
method,
|
|
2430
|
+
headers: {
|
|
2431
|
+
Authorization: `Bearer ${token}`,
|
|
2432
|
+
"Content-Type": "application/json"
|
|
2433
|
+
},
|
|
2434
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
2435
|
+
});
|
|
2436
|
+
const data = await response.json();
|
|
2437
|
+
if (!response.ok) {
|
|
2438
|
+
const errData = data;
|
|
2439
|
+
const message = errData.error?.message ?? `HTTP ${response.status}`;
|
|
2440
|
+
throw new GcpApiError(message, response.status);
|
|
2441
|
+
}
|
|
2442
|
+
return data;
|
|
2443
|
+
}
|
|
2444
|
+
async function pollOperation(operationName, token, maxWaitMs = 6e4) {
|
|
2445
|
+
const opUrl = operationName.startsWith("http") ? operationName : `https://iam.googleapis.com/v1/${operationName}`;
|
|
2446
|
+
const deadline = Date.now() + maxWaitMs;
|
|
2447
|
+
const maxWaitSecs = Math.round(maxWaitMs / 1e3);
|
|
2448
|
+
while (Date.now() < deadline) {
|
|
2449
|
+
const op = await gcpFetch(opUrl, token);
|
|
2450
|
+
if (op.done) {
|
|
2451
|
+
if (op.error) {
|
|
2452
|
+
throw new Error(`Operation failed: ${op.error.message}`);
|
|
2453
|
+
}
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
2457
|
+
}
|
|
2458
|
+
if (Date.now() >= deadline) {
|
|
2459
|
+
throw new Error(
|
|
2460
|
+
`Operation timed out after ${maxWaitSecs} s. The resources may still be provisioning in the background \u2013 re-running the command is safe (existing resources are reused).`
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
async function getProjectNumber(projectId, token) {
|
|
2465
|
+
const data = await gcpFetch(
|
|
2466
|
+
`https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(projectId)}`,
|
|
2467
|
+
token
|
|
2468
|
+
);
|
|
2469
|
+
return data.projectNumber;
|
|
2470
|
+
}
|
|
2471
|
+
async function createOrGetWifPool(projectId, poolId, token) {
|
|
2472
|
+
try {
|
|
2473
|
+
const op = await gcpFetch(
|
|
2474
|
+
`https://iam.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/locations/global/workloadIdentityPools?workloadIdentityPoolId=${encodeURIComponent(poolId)}`,
|
|
2475
|
+
token,
|
|
2476
|
+
"POST",
|
|
2477
|
+
{
|
|
2478
|
+
displayName: "GitHub Actions Pool",
|
|
2479
|
+
description: "Pool for GitHub Actions OIDC authentication",
|
|
2480
|
+
disabled: false
|
|
2481
|
+
}
|
|
2482
|
+
);
|
|
2483
|
+
if (!op.done) {
|
|
2484
|
+
await pollOperation(op.name, token);
|
|
2485
|
+
} else if (op.error) {
|
|
2486
|
+
throw new Error(`Operation failed: ${op.error.message}`);
|
|
2487
|
+
}
|
|
2488
|
+
} catch (err) {
|
|
2489
|
+
if (err instanceof GcpApiError && err.status === 409) {
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
throw err;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
async function createOrGetWifProvider(projectId, poolId, providerId, githubRepo, token) {
|
|
2496
|
+
try {
|
|
2497
|
+
const op = await gcpFetch(
|
|
2498
|
+
`https://iam.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/locations/global/workloadIdentityPools/${encodeURIComponent(poolId)}/providers?workloadIdentityPoolProviderId=${encodeURIComponent(providerId)}`,
|
|
2499
|
+
token,
|
|
2500
|
+
"POST",
|
|
2501
|
+
{
|
|
2502
|
+
displayName: "GitHub OIDC Provider",
|
|
2503
|
+
disabled: false,
|
|
2504
|
+
attributeMapping: {
|
|
2505
|
+
"google.subject": "assertion.sub",
|
|
2506
|
+
"attribute.actor": "assertion.actor",
|
|
2507
|
+
"attribute.repository": "assertion.repository"
|
|
2508
|
+
},
|
|
2509
|
+
// Scope the provider to this exact repository for security
|
|
2510
|
+
attributeCondition: `assertion.repository=='${githubRepo}'`,
|
|
2511
|
+
oidc: {
|
|
2512
|
+
issuerUri: "https://token.actions.githubusercontent.com"
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
);
|
|
2516
|
+
if (!op.done) {
|
|
2517
|
+
await pollOperation(op.name, token);
|
|
2518
|
+
} else if (op.error) {
|
|
2519
|
+
throw new Error(`Operation failed: ${op.error.message}`);
|
|
2520
|
+
}
|
|
2521
|
+
} catch (err) {
|
|
2522
|
+
if (err instanceof GcpApiError && err.status === 409) {
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
throw err;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
async function bindServiceAccount(projectId, serviceAccountEmail, projectNumber, poolId, githubRepo, token) {
|
|
2529
|
+
const saResource = `projects/${encodeURIComponent(projectId)}/serviceAccounts/${encodeURIComponent(serviceAccountEmail)}`;
|
|
2530
|
+
const principal = `principalSet://iam.googleapis.com/projects/${projectNumber}/locations/global/workloadIdentityPools/${poolId}/attribute.repository/${githubRepo}`;
|
|
2531
|
+
const policy = await gcpFetch(
|
|
2532
|
+
`https://iam.googleapis.com/v1/${saResource}:getIamPolicy`,
|
|
2533
|
+
token,
|
|
2534
|
+
"POST",
|
|
2535
|
+
{}
|
|
2536
|
+
);
|
|
2537
|
+
const bindings = policy.bindings ?? [];
|
|
2538
|
+
const role = "roles/iam.workloadIdentityUser";
|
|
2539
|
+
const existing = bindings.find((b) => b.role === role);
|
|
2540
|
+
if (existing) {
|
|
2541
|
+
if (existing.members.includes(principal)) {
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
existing.members.push(principal);
|
|
2545
|
+
} else {
|
|
2546
|
+
bindings.push({ role, members: [principal] });
|
|
2547
|
+
}
|
|
2548
|
+
await gcpFetch(
|
|
2549
|
+
`https://iam.googleapis.com/v1/${saResource}:setIamPolicy`,
|
|
2550
|
+
token,
|
|
2551
|
+
"POST",
|
|
2552
|
+
{ policy: { ...policy, bindings } }
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
async function setupWIF(options) {
|
|
2556
|
+
const poolId = options.poolId ?? "github-actions";
|
|
2557
|
+
const providerId = options.providerId ?? "github-oidc";
|
|
2558
|
+
const log = options.onProgress ?? (() => void 0);
|
|
2559
|
+
if (!options.githubRepo.includes("/")) {
|
|
2560
|
+
throw new Error(
|
|
2561
|
+
`githubRepo must be in "owner/repo" format, got: "${options.githubRepo}"`
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
log("Authenticating with Google Cloud...");
|
|
2565
|
+
const token = await getAccessToken4(options.keyFilePath);
|
|
2566
|
+
log("Fetching project number...");
|
|
2567
|
+
const projectNumber = await getProjectNumber(options.projectId, token);
|
|
2568
|
+
log(`Creating Workload Identity Pool "${poolId}"...`);
|
|
2569
|
+
await createOrGetWifPool(options.projectId, poolId, token);
|
|
2570
|
+
log(`Creating OIDC Provider "${providerId}"...`);
|
|
2571
|
+
await createOrGetWifProvider(
|
|
2572
|
+
options.projectId,
|
|
2573
|
+
poolId,
|
|
2574
|
+
providerId,
|
|
2575
|
+
options.githubRepo,
|
|
2576
|
+
token
|
|
2577
|
+
);
|
|
2578
|
+
log("Binding service account permissions...");
|
|
2579
|
+
await bindServiceAccount(
|
|
2580
|
+
options.projectId,
|
|
2581
|
+
options.serviceAccountEmail,
|
|
2582
|
+
projectNumber,
|
|
2583
|
+
poolId,
|
|
2584
|
+
options.githubRepo,
|
|
2585
|
+
token
|
|
2586
|
+
);
|
|
2587
|
+
const wifProvider = `projects/${projectNumber}/locations/global/workloadIdentityPools/${poolId}/providers/${providerId}`;
|
|
2588
|
+
return { wifProvider, projectNumber, poolId, providerId };
|
|
2589
|
+
}
|
|
2590
|
+
async function grantDrivePermissions(options) {
|
|
2591
|
+
const token = await getAccessToken4(options.keyFilePath);
|
|
2592
|
+
const policy = await gcpFetch(
|
|
2593
|
+
`https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(options.projectId)}:getIamPolicy`,
|
|
2594
|
+
token,
|
|
2595
|
+
"POST",
|
|
2596
|
+
{}
|
|
2597
|
+
);
|
|
2598
|
+
const bindings = policy.bindings ?? [];
|
|
2599
|
+
const role = "roles/drive.file";
|
|
2600
|
+
const member = `serviceAccount:${options.serviceAccountEmail}`;
|
|
2601
|
+
const existing = bindings.find((b) => b.role === role);
|
|
2602
|
+
if (existing) {
|
|
2603
|
+
if (existing.members.includes(member)) {
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
existing.members.push(member);
|
|
2607
|
+
} else {
|
|
2608
|
+
bindings.push({ role, members: [member] });
|
|
2609
|
+
}
|
|
2610
|
+
await gcpFetch(
|
|
2611
|
+
`https://cloudresourcemanager.googleapis.com/v1/projects/${encodeURIComponent(options.projectId)}:setIamPolicy`,
|
|
2612
|
+
token,
|
|
2613
|
+
"POST",
|
|
2614
|
+
{ policy: { ...policy, bindings } }
|
|
2615
|
+
);
|
|
1946
2616
|
}
|
|
1947
2617
|
|
|
1948
2618
|
// src/index.ts
|
|
@@ -1950,6 +2620,7 @@ var index_default = getSpreadSheetData;
|
|
|
1950
2620
|
export {
|
|
1951
2621
|
DEFAULT_IMAGE_EXTENSIONS,
|
|
1952
2622
|
DEFAULT_WAIT_SECONDS,
|
|
2623
|
+
GcpApiError,
|
|
1953
2624
|
buildGoogleAuth,
|
|
1954
2625
|
buildManifest,
|
|
1955
2626
|
convertFromDataJsonFormat,
|
|
@@ -1958,6 +2629,9 @@ export {
|
|
|
1958
2629
|
createLocaleMapping,
|
|
1959
2630
|
createSpreadsheet,
|
|
1960
2631
|
index_default as default,
|
|
2632
|
+
entriesToSeedKeys,
|
|
2633
|
+
entriesToTranslationData,
|
|
2634
|
+
exportDoc,
|
|
1961
2635
|
filterValidLocales,
|
|
1962
2636
|
findLocalChanges,
|
|
1963
2637
|
getGoogleTranslateCode,
|
|
@@ -1968,7 +2642,10 @@ export {
|
|
|
1968
2642
|
getOriginalHeaderForLocale,
|
|
1969
2643
|
getSpreadSheetData,
|
|
1970
2644
|
getTranslationSummary,
|
|
2645
|
+
grantDrivePermissions,
|
|
1971
2646
|
handleBidirectionalSync,
|
|
2647
|
+
inferLocaleFromDocName,
|
|
2648
|
+
ingestDoc,
|
|
1972
2649
|
isValidLocale,
|
|
1973
2650
|
manageDriveTranslations,
|
|
1974
2651
|
mergeMultipleTranslationData,
|
|
@@ -1976,10 +2653,15 @@ export {
|
|
|
1976
2653
|
normalizeExtension,
|
|
1977
2654
|
normalizeLocaleCode,
|
|
1978
2655
|
normalizePrivateKey,
|
|
2656
|
+
parseDocContent,
|
|
1979
2657
|
processRawRows,
|
|
2658
|
+
readManifest,
|
|
1980
2659
|
readPublicSheet,
|
|
1981
2660
|
resolveLocaleWithFallback,
|
|
2661
|
+
scanDriveFolderForDocs,
|
|
1982
2662
|
scanDriveFolderForSpreadsheets,
|
|
2663
|
+
setupWIF,
|
|
2664
|
+
slugifyKey,
|
|
1983
2665
|
syncDriveImages,
|
|
1984
2666
|
updateSpreadsheetWithLocalChanges,
|
|
1985
2667
|
validateCredentials,
|