@el-j/google-sheet-translations 1.3.3 → 2.0.0
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 +58 -55
- package/dist/action-entrypoint.d.ts +2 -0
- package/dist/action-entrypoint.d.ts.map +1 -0
- package/dist/esm/index.js +1226 -0
- package/dist/esm/package.json +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1288 -41
- package/dist/utils/dataConverter/findLocalChanges.d.ts +2 -1
- package/dist/utils/dataConverter/findLocalChanges.d.ts.map +1 -1
- package/dist/utils/localeNormalizer.d.ts +31 -0
- package/dist/utils/localeNormalizer.d.ts.map +1 -1
- package/dist/utils/translationHelpers.d.ts +107 -0
- package/dist/utils/translationHelpers.d.ts.map +1 -0
- package/package.json +21 -14
- package/dist/constants.js +0 -9
- package/dist/constants.js.map +0 -1
- package/dist/getSpreadSheetData.js +0 -170
- package/dist/getSpreadSheetData.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/utils/auth.js +0 -23
- package/dist/utils/auth.js.map +0 -1
- package/dist/utils/configurationHandler.js +0 -29
- package/dist/utils/configurationHandler.js.map +0 -1
- package/dist/utils/dataConverter/convertFromDataJsonFormat.js +0 -47
- package/dist/utils/dataConverter/convertFromDataJsonFormat.js.map +0 -1
- package/dist/utils/dataConverter/convertToDataJsonFormat.js +0 -51
- package/dist/utils/dataConverter/convertToDataJsonFormat.js.map +0 -1
- package/dist/utils/dataConverter/findLocalChanges.js +0 -70
- package/dist/utils/dataConverter/findLocalChanges.js.map +0 -1
- package/dist/utils/fileWriter.js +0 -119
- package/dist/utils/fileWriter.js.map +0 -1
- package/dist/utils/getFileLastModified.js +0 -23
- package/dist/utils/getFileLastModified.js.map +0 -1
- package/dist/utils/isDataJsonNewer.js +0 -40
- package/dist/utils/isDataJsonNewer.js.map +0 -1
- package/dist/utils/localeFilter.js +0 -49
- package/dist/utils/localeFilter.js.map +0 -1
- package/dist/utils/localeNormalizer.js +0 -176
- package/dist/utils/localeNormalizer.js.map +0 -1
- package/dist/utils/publicSheetReader.js +0 -109
- package/dist/utils/publicSheetReader.js.map +0 -1
- package/dist/utils/rateLimiter.js +0 -55
- package/dist/utils/rateLimiter.js.map +0 -1
- package/dist/utils/readDataJson.js +0 -29
- package/dist/utils/readDataJson.js.map +0 -1
- package/dist/utils/sheetProcessor.js +0 -121
- package/dist/utils/sheetProcessor.js.map +0 -1
- package/dist/utils/spreadsheetCreator.js +0 -121
- package/dist/utils/spreadsheetCreator.js.map +0 -1
- package/dist/utils/spreadsheetUpdater.js +0 -227
- package/dist/utils/spreadsheetUpdater.js.map +0 -1
- package/dist/utils/syncManager.js +0 -62
- package/dist/utils/syncManager.js.map +0 -1
- package/dist/utils/validateEnv.js +0 -41
- package/dist/utils/validateEnv.js.map +0 -1
- package/dist/utils/wait.js +0 -19
- package/dist/utils/wait.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,44 +1,1291 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
DEFAULT_WAIT_SECONDS: () => DEFAULT_WAIT_SECONDS,
|
|
34
|
+
convertFromDataJsonFormat: () => convertFromDataJsonFormat,
|
|
35
|
+
convertToDataJsonFormat: () => convertToDataJsonFormat,
|
|
36
|
+
createAuthClient: () => createAuthClient,
|
|
37
|
+
createLocaleMapping: () => createLocaleMapping,
|
|
38
|
+
createSpreadsheet: () => createSpreadsheet,
|
|
39
|
+
default: () => index_default,
|
|
40
|
+
filterValidLocales: () => filterValidLocales,
|
|
41
|
+
findLocalChanges: () => findLocalChanges,
|
|
42
|
+
getLanguagePrefix: () => getLanguagePrefix,
|
|
43
|
+
getLocaleDisplayName: () => getLocaleDisplayName,
|
|
44
|
+
getNormalizedLocaleForHeader: () => getNormalizedLocaleForHeader,
|
|
45
|
+
getOriginalHeaderForLocale: () => getOriginalHeaderForLocale,
|
|
46
|
+
getSpreadSheetData: () => getSpreadSheetData,
|
|
47
|
+
getTranslationSummary: () => getTranslationSummary,
|
|
48
|
+
handleBidirectionalSync: () => handleBidirectionalSync,
|
|
49
|
+
isValidLocale: () => isValidLocale,
|
|
50
|
+
mergeSheets: () => mergeSheets,
|
|
51
|
+
normalizeLocaleCode: () => normalizeLocaleCode,
|
|
52
|
+
processRawRows: () => processRawRows,
|
|
53
|
+
readPublicSheet: () => readPublicSheet,
|
|
54
|
+
resolveLocaleWithFallback: () => resolveLocaleWithFallback,
|
|
55
|
+
updateSpreadsheetWithLocalChanges: () => updateSpreadsheetWithLocalChanges,
|
|
56
|
+
validateCredentials: () => validateCredentials,
|
|
57
|
+
validateEnv: () => validateEnv,
|
|
58
|
+
wait: () => wait,
|
|
59
|
+
withRetry: () => withRetry,
|
|
60
|
+
writeLanguageDataFile: () => writeLanguageDataFile,
|
|
61
|
+
writeLocalesFile: () => writeLocalesFile,
|
|
62
|
+
writeTranslationFiles: () => writeTranslationFiles
|
|
63
|
+
});
|
|
64
|
+
module.exports = __toCommonJS(index_exports);
|
|
65
|
+
|
|
66
|
+
// src/getSpreadSheetData.ts
|
|
67
|
+
var import_node_fs5 = __toESM(require("node:fs"));
|
|
68
|
+
var import_node_path4 = __toESM(require("node:path"));
|
|
69
|
+
var import_google_spreadsheet2 = require("google-spreadsheet");
|
|
70
|
+
|
|
71
|
+
// src/utils/auth.ts
|
|
72
|
+
var import_google_auth_library = require("google-auth-library");
|
|
73
|
+
|
|
74
|
+
// src/utils/validateEnv.ts
|
|
75
|
+
function validateCredentials() {
|
|
76
|
+
const requiredVars = ["GOOGLE_CLIENT_EMAIL", "GOOGLE_PRIVATE_KEY"];
|
|
77
|
+
const missing = requiredVars.filter((v) => !process.env[v]);
|
|
78
|
+
if (missing.length > 0) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Missing required environment variables: ${missing.join(", ")}
|
|
81
|
+
|
|
82
|
+
Make sure these are set in your .env file or environment.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
GOOGLE_CLIENT_EMAIL: process.env.GOOGLE_CLIENT_EMAIL,
|
|
87
|
+
GOOGLE_PRIVATE_KEY: process.env.GOOGLE_PRIVATE_KEY
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function validateEnv() {
|
|
91
|
+
const requiredVars = [
|
|
92
|
+
"GOOGLE_CLIENT_EMAIL",
|
|
93
|
+
"GOOGLE_PRIVATE_KEY",
|
|
94
|
+
"GOOGLE_SPREADSHEET_ID"
|
|
95
|
+
];
|
|
96
|
+
const missingVars = requiredVars.filter((varName) => !process.env[varName]);
|
|
97
|
+
if (missingVars.length > 0) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Missing required environment variables: ${missingVars.join(", ")}
|
|
100
|
+
|
|
101
|
+
Make sure these are set in your .env file or environment.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
GOOGLE_CLIENT_EMAIL: process.env.GOOGLE_CLIENT_EMAIL,
|
|
106
|
+
GOOGLE_PRIVATE_KEY: process.env.GOOGLE_PRIVATE_KEY,
|
|
107
|
+
GOOGLE_SPREADSHEET_ID: process.env.GOOGLE_SPREADSHEET_ID
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/utils/auth.ts
|
|
112
|
+
function createAuthClient() {
|
|
113
|
+
const { GOOGLE_CLIENT_EMAIL, GOOGLE_PRIVATE_KEY } = validateCredentials();
|
|
114
|
+
const normalizedKey = GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n");
|
|
115
|
+
return new import_google_auth_library.JWT({
|
|
116
|
+
email: GOOGLE_CLIENT_EMAIL,
|
|
117
|
+
key: normalizedKey,
|
|
118
|
+
scopes: ["https://www.googleapis.com/auth/spreadsheets"]
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/utils/configurationHandler.ts
|
|
123
|
+
var import_node_path = __toESM(require("node:path"));
|
|
124
|
+
|
|
125
|
+
// src/constants.ts
|
|
126
|
+
var DEFAULT_WAIT_SECONDS = 1;
|
|
127
|
+
|
|
128
|
+
// src/utils/configurationHandler.ts
|
|
129
|
+
function normalizeConfig(options = {}) {
|
|
130
|
+
return {
|
|
131
|
+
rowLimit: options.rowLimit ?? 100,
|
|
132
|
+
waitSeconds: options.waitSeconds ?? DEFAULT_WAIT_SECONDS,
|
|
133
|
+
dataJsonPath: options.dataJsonPath ?? import_node_path.default.join(process.cwd(), "src/lib/languageData.json"),
|
|
134
|
+
localesOutputPath: options.localesOutputPath ?? import_node_path.default.join(process.cwd(), "src/i18n/locales.ts"),
|
|
135
|
+
translationsOutputDir: options.translationsOutputDir ?? import_node_path.default.join(process.cwd(), "translations"),
|
|
136
|
+
syncLocalChanges: options.syncLocalChanges !== false,
|
|
137
|
+
// Default to true
|
|
138
|
+
autoTranslate: options.autoTranslate === true,
|
|
139
|
+
// Default to false
|
|
140
|
+
spreadsheetId: options.spreadsheetId,
|
|
141
|
+
publicSheet: options.publicSheet === true,
|
|
142
|
+
// Default to false
|
|
143
|
+
autoCreate: options.autoCreate !== false,
|
|
144
|
+
// Default to true
|
|
145
|
+
spreadsheetTitle: options.spreadsheetTitle ?? "google-sheet-translations",
|
|
146
|
+
sourceLocale: options.sourceLocale ?? "en",
|
|
147
|
+
targetLocales: options.targetLocales ?? ["de", "fr", "es", "it", "pt", "ja", "zh"]
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/utils/rateLimiter.ts
|
|
152
|
+
var import_promises = require("node:timers/promises");
|
|
153
|
+
var DEFAULT_RETRIES = 3;
|
|
154
|
+
var DEFAULT_MAX_DELAY_MS = 3e4;
|
|
155
|
+
function isRateLimitError(err) {
|
|
156
|
+
if (!err || typeof err !== "object") return false;
|
|
157
|
+
const e = err;
|
|
158
|
+
const response = e["response"];
|
|
159
|
+
const status = typeof e["status"] === "number" ? e["status"] : typeof response?.["status"] === "number" ? response["status"] : void 0;
|
|
160
|
+
return status === 429 || status === 503;
|
|
161
|
+
}
|
|
162
|
+
async function withRetry(fn, label, baseDelayMs = 1e3, retries = DEFAULT_RETRIES, maxDelayMs = DEFAULT_MAX_DELAY_MS) {
|
|
163
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
164
|
+
try {
|
|
165
|
+
return await fn();
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (!isRateLimitError(err) || attempt === retries) throw err;
|
|
168
|
+
const backoff = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
169
|
+
console.warn(
|
|
170
|
+
`[rate-limit] ${label}: retry ${attempt + 1}/${retries} in ${backoff} ms`
|
|
171
|
+
);
|
|
172
|
+
await (0, import_promises.setTimeout)(backoff);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw new Error("withRetry: unreachable");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/utils/localeFilter.ts
|
|
179
|
+
var COMMON_LOCALE_PATTERNS = [
|
|
180
|
+
/^[a-z]{2}$/,
|
|
181
|
+
// en, de, fr
|
|
182
|
+
/^[a-z]{2}-[a-z]{2}$/,
|
|
183
|
+
// en-us, de-de
|
|
184
|
+
/^[a-z]{2}_[a-z]{2}$/,
|
|
185
|
+
// en_us, de_de
|
|
186
|
+
/^[a-z]{2}-[a-z]{2}-[a-z]+$/
|
|
187
|
+
// en-us-traditional
|
|
188
|
+
];
|
|
189
|
+
var NON_LOCALE_KEYWORDS = [
|
|
190
|
+
"key",
|
|
191
|
+
"keys",
|
|
192
|
+
"id",
|
|
193
|
+
"identifier",
|
|
194
|
+
"name",
|
|
195
|
+
"title",
|
|
196
|
+
"label",
|
|
197
|
+
"description",
|
|
198
|
+
"comment",
|
|
199
|
+
"note",
|
|
200
|
+
"context",
|
|
201
|
+
"category",
|
|
202
|
+
"type",
|
|
203
|
+
"status",
|
|
204
|
+
"updated",
|
|
205
|
+
"created",
|
|
206
|
+
"modified",
|
|
207
|
+
"version",
|
|
208
|
+
"source",
|
|
209
|
+
"i18n",
|
|
210
|
+
"translation",
|
|
211
|
+
"namespace",
|
|
212
|
+
"section"
|
|
213
|
+
];
|
|
214
|
+
function isValidLocale(value) {
|
|
215
|
+
if (!value || typeof value !== "string") {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
const normalized = value.toLowerCase().trim();
|
|
219
|
+
if (NON_LOCALE_KEYWORDS.includes(normalized)) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
return COMMON_LOCALE_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
223
|
+
}
|
|
224
|
+
function filterValidLocales(headerRow, keyColumn) {
|
|
225
|
+
return headerRow.filter((column) => column.toLowerCase() !== keyColumn.toLowerCase()).filter((column) => isValidLocale(column)).map((locale) => locale.toLowerCase());
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/utils/localeNormalizer.ts
|
|
229
|
+
function getLanguagePrefix(locale) {
|
|
230
|
+
return locale.toLowerCase().split(/[-_]/)[0];
|
|
231
|
+
}
|
|
232
|
+
var LANGUAGE_TO_COUNTRY_MAP = {
|
|
233
|
+
"en": "en-GB",
|
|
234
|
+
"de": "de-DE",
|
|
235
|
+
"fr": "fr-FR",
|
|
236
|
+
"es": "es-ES",
|
|
237
|
+
"it": "it-IT",
|
|
238
|
+
"pt": "pt-PT",
|
|
239
|
+
"pl": "pl-PL",
|
|
240
|
+
"ru": "ru-RU",
|
|
241
|
+
"zh": "zh-CN",
|
|
242
|
+
"ja": "ja-JP",
|
|
243
|
+
"ko": "ko-KR",
|
|
244
|
+
"ar": "ar-SA",
|
|
245
|
+
"hi": "hi-IN",
|
|
246
|
+
"th": "th-TH",
|
|
247
|
+
"vi": "vi-VN",
|
|
248
|
+
"tr": "tr-TR",
|
|
249
|
+
"nl": "nl-NL",
|
|
250
|
+
"sv": "sv-SE",
|
|
251
|
+
"da": "da-DK",
|
|
252
|
+
"no": "no-NO",
|
|
253
|
+
"fi": "fi-FI",
|
|
254
|
+
"cs": "cs-CZ",
|
|
255
|
+
"sk": "sk-SK",
|
|
256
|
+
"hu": "hu-HU",
|
|
257
|
+
"ro": "ro-RO",
|
|
258
|
+
"bg": "bg-BG",
|
|
259
|
+
"hr": "hr-HR",
|
|
260
|
+
"sl": "sl-SI",
|
|
261
|
+
"et": "et-EE",
|
|
262
|
+
"lv": "lv-LV",
|
|
263
|
+
"lt": "lt-LT",
|
|
264
|
+
"el": "el-GR",
|
|
265
|
+
"he": "he-IL",
|
|
266
|
+
"uk": "uk-UA",
|
|
267
|
+
"be": "be-BY"
|
|
268
|
+
};
|
|
269
|
+
function normalizeLocaleCode(locale) {
|
|
270
|
+
if (!locale || typeof locale !== "string") {
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
const normalized = locale.toLowerCase().trim();
|
|
274
|
+
if (normalized.includes("-") || normalized.includes("_")) {
|
|
275
|
+
return normalized;
|
|
276
|
+
}
|
|
277
|
+
const withCountry = LANGUAGE_TO_COUNTRY_MAP[normalized];
|
|
278
|
+
if (withCountry) {
|
|
279
|
+
return withCountry;
|
|
280
|
+
}
|
|
281
|
+
if (normalized.length === 2 && /^[a-z]{2}$/.test(normalized)) {
|
|
282
|
+
return `${normalized}-${normalized.toUpperCase()}`;
|
|
283
|
+
}
|
|
284
|
+
return normalized;
|
|
285
|
+
}
|
|
286
|
+
function createLocaleMapping(originalHeaders, keyColumn) {
|
|
287
|
+
const localeMapping = {};
|
|
288
|
+
const originalMapping = {};
|
|
289
|
+
const normalizedLocales = [];
|
|
290
|
+
for (const header of originalHeaders) {
|
|
291
|
+
const headerLower = header.toLowerCase();
|
|
292
|
+
if (headerLower === keyColumn.toLowerCase()) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const isLocale = /^[a-z]{2}([_-][a-z]{2})?([_-][a-z]+)?$/.test(headerLower);
|
|
296
|
+
if (!isLocale) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const normalized = normalizeLocaleCode(headerLower);
|
|
300
|
+
localeMapping[normalized] = header;
|
|
301
|
+
originalMapping[headerLower] = normalized;
|
|
302
|
+
normalizedLocales.push(normalized);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
normalizedLocales: [...new Set(normalizedLocales)],
|
|
306
|
+
// Remove duplicates
|
|
307
|
+
localeMapping,
|
|
308
|
+
// normalized -> original header
|
|
309
|
+
originalMapping
|
|
310
|
+
// original header (lowercase) -> normalized
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function getOriginalHeaderForLocale(normalizedLocale, localeMapping) {
|
|
314
|
+
let result = localeMapping[normalizedLocale];
|
|
315
|
+
if (result) return result;
|
|
316
|
+
const lowercaseLocale = normalizedLocale.toLowerCase();
|
|
317
|
+
result = localeMapping[lowercaseLocale];
|
|
318
|
+
if (result) return result;
|
|
319
|
+
for (const [key, value] of Object.entries(localeMapping)) {
|
|
320
|
+
if (key.toLowerCase() === lowercaseLocale) {
|
|
321
|
+
return value;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const inputLangCode = getLanguagePrefix(normalizedLocale);
|
|
325
|
+
for (const [key, value] of Object.entries(localeMapping)) {
|
|
326
|
+
if (getLanguagePrefix(key) === inputLangCode) {
|
|
327
|
+
return value;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return void 0;
|
|
331
|
+
}
|
|
332
|
+
function getNormalizedLocaleForHeader(originalHeader, originalMapping) {
|
|
333
|
+
return originalMapping[originalHeader.toLowerCase()];
|
|
334
|
+
}
|
|
335
|
+
function resolveLocaleWithFallback(locale, availableLocales) {
|
|
336
|
+
if (availableLocales.includes(locale)) return locale;
|
|
337
|
+
const lower = locale.toLowerCase();
|
|
338
|
+
const lowerMatch = availableLocales.find((l) => l === lower);
|
|
339
|
+
if (lowerMatch) return lowerMatch;
|
|
340
|
+
const langCode = getLanguagePrefix(lower);
|
|
341
|
+
return availableLocales.find((l) => getLanguagePrefix(l) === langCode);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/utils/sheetProcessor.ts
|
|
345
|
+
async function processRawRows(rows, sheetTitle) {
|
|
346
|
+
const result = {
|
|
347
|
+
translations: {},
|
|
348
|
+
locales: [],
|
|
349
|
+
localeMapping: {},
|
|
350
|
+
originalMapping: {},
|
|
351
|
+
success: false
|
|
352
|
+
};
|
|
353
|
+
try {
|
|
354
|
+
if (!rows || rows.length === 0) {
|
|
355
|
+
console.warn(`No rows found in sheet "${sheetTitle}"`);
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
const headerRow = Object.keys(rows[0]).map((key) => key.toLowerCase());
|
|
359
|
+
console.log(`Header row for sheet "${sheetTitle}":`, headerRow);
|
|
360
|
+
const keyColumn = headerRow[0];
|
|
361
|
+
const validLocales = filterValidLocales(headerRow, keyColumn);
|
|
362
|
+
if (validLocales.length === 0) {
|
|
363
|
+
console.warn(`No valid locale columns found in sheet "${sheetTitle}"`);
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
const originalHeaders = Object.keys(rows[0]);
|
|
367
|
+
const { normalizedLocales, localeMapping, originalMapping } = createLocaleMapping(
|
|
368
|
+
originalHeaders,
|
|
369
|
+
keyColumn
|
|
370
|
+
);
|
|
371
|
+
result.localeMapping = localeMapping;
|
|
372
|
+
result.originalMapping = originalMapping;
|
|
373
|
+
for (const normalizedLocale of normalizedLocales) {
|
|
374
|
+
const originalHeader = localeMapping[normalizedLocale];
|
|
375
|
+
if (!originalHeader) continue;
|
|
376
|
+
const languageCells = rows.map((row) => {
|
|
377
|
+
const keyField = Object.keys(row).find((k) => k.toLowerCase() === keyColumn);
|
|
378
|
+
if (!keyField || !row[keyField] || !row[originalHeader]) {
|
|
379
|
+
return {};
|
|
380
|
+
}
|
|
381
|
+
const rowLocal = {};
|
|
382
|
+
rowLocal[row[keyField].toString().toLowerCase()] = row[originalHeader];
|
|
383
|
+
return rowLocal;
|
|
384
|
+
});
|
|
385
|
+
const nonEmptyLanguageCells = languageCells.filter(
|
|
386
|
+
(cell) => Object.keys(cell).length > 0
|
|
387
|
+
);
|
|
388
|
+
const prepareObj = {};
|
|
389
|
+
prepareObj[sheetTitle] = nonEmptyLanguageCells.reduce(
|
|
390
|
+
(acc, cell) => Object.assign(acc, cell),
|
|
391
|
+
{}
|
|
392
|
+
);
|
|
393
|
+
if (result.translations[normalizedLocale]) {
|
|
394
|
+
result.translations[normalizedLocale] = {
|
|
395
|
+
...result.translations[normalizedLocale],
|
|
396
|
+
...prepareObj
|
|
397
|
+
};
|
|
398
|
+
} else {
|
|
399
|
+
result.translations[normalizedLocale] = { ...prepareObj };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
result.locales = normalizedLocales;
|
|
403
|
+
result.success = true;
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error(`Error processing sheet "${sheetTitle}":`, error);
|
|
406
|
+
}
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
async function processSheet(sheet, sheetTitle, rowLimit, baseDelayMs = 1e3) {
|
|
410
|
+
const emptyResult = {
|
|
411
|
+
translations: {},
|
|
412
|
+
locales: [],
|
|
413
|
+
localeMapping: {},
|
|
414
|
+
originalMapping: {},
|
|
415
|
+
success: false
|
|
416
|
+
};
|
|
417
|
+
try {
|
|
418
|
+
const googleRows = await withRetry(
|
|
419
|
+
() => sheet.getRows({ limit: rowLimit }),
|
|
420
|
+
`getRows: ${sheetTitle}`,
|
|
421
|
+
baseDelayMs
|
|
422
|
+
);
|
|
423
|
+
if (!googleRows || googleRows.length === 0) {
|
|
424
|
+
console.warn(`No rows found in sheet "${sheetTitle}"`);
|
|
425
|
+
return emptyResult;
|
|
426
|
+
}
|
|
427
|
+
const rows = googleRows.map((row) => row.toObject());
|
|
428
|
+
return processRawRows(rows, sheetTitle);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error(`Error processing sheet "${sheetTitle}":`, error);
|
|
431
|
+
return emptyResult;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/utils/fileWriter.ts
|
|
436
|
+
var import_node_fs = __toESM(require("node:fs"));
|
|
437
|
+
var import_node_path2 = __toESM(require("node:path"));
|
|
438
|
+
|
|
439
|
+
// src/utils/dataConverter/convertToDataJsonFormat.ts
|
|
440
|
+
function convertToDataJsonFormat(translationObj, locales) {
|
|
441
|
+
const result = [];
|
|
442
|
+
console.log("Converting translation object to languageData.json format...");
|
|
443
|
+
const allSheets = /* @__PURE__ */ new Set();
|
|
444
|
+
for (const locale of Object.keys(translationObj)) {
|
|
445
|
+
if (translationObj[locale]) {
|
|
446
|
+
for (const sheet of Object.keys(translationObj[locale])) {
|
|
447
|
+
allSheets.add(sheet);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
console.log(`Found ${allSheets.size} sheets across all locales`);
|
|
452
|
+
for (const sheetTitle of allSheets) {
|
|
453
|
+
const projectData = {};
|
|
454
|
+
projectData[sheetTitle] = {};
|
|
455
|
+
for (const locale of locales) {
|
|
456
|
+
if (translationObj?.[locale]?.[sheetTitle]) {
|
|
457
|
+
projectData[sheetTitle][locale] = {};
|
|
458
|
+
const translations = translationObj[locale][sheetTitle];
|
|
459
|
+
for (const key of Object.keys(translations)) {
|
|
460
|
+
projectData[sheetTitle][locale][key] = translations[key];
|
|
461
|
+
}
|
|
462
|
+
console.log(
|
|
463
|
+
`Found ${Object.keys(translations).length} keys for locale ${locale} in sheet ${sheetTitle}`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (Object.keys(projectData[sheetTitle]).length > 0) {
|
|
468
|
+
result.push(projectData);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
console.log(`Created ${result.length} sheet entries for languageData.json`);
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/utils/fileWriter.ts
|
|
476
|
+
function writeTranslationFiles(translations, locales, translationsOutputDir) {
|
|
477
|
+
if (!import_node_fs.default.existsSync(translationsOutputDir)) {
|
|
478
|
+
try {
|
|
479
|
+
import_node_fs.default.mkdirSync(translationsOutputDir, { recursive: true });
|
|
480
|
+
} catch (err) {
|
|
481
|
+
throw new Error(`Failed to create translations directory "${translationsOutputDir}"`, { cause: err });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
for (const locale of locales) {
|
|
485
|
+
if (!translations[locale] || Object.keys(translations[locale]).length === 0) {
|
|
486
|
+
console.warn(`No translations found for locale "${locale}"`);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const safeName = locale.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
|
|
490
|
+
if (safeName !== locale.toLowerCase()) {
|
|
491
|
+
console.warn(`Locale "${locale}" contained unsafe characters; sanitised to "${safeName}"`);
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
import_node_fs.default.writeFileSync(
|
|
495
|
+
import_node_path2.default.join(translationsOutputDir, `${safeName}.json`),
|
|
496
|
+
JSON.stringify(translations[locale], null, 2),
|
|
497
|
+
"utf8"
|
|
498
|
+
);
|
|
499
|
+
console.log(`Successfully wrote translations for ${locale}`);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
console.error(
|
|
502
|
+
`Failed to write translation file for locale "${locale}" at "${import_node_path2.default.join(translationsOutputDir, `${safeName}.json`)}":`,
|
|
503
|
+
err
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function writeLocalesFile(locales, localeMapping, localesOutputPath) {
|
|
509
|
+
const localesOutputDir = import_node_path2.default.dirname(localesOutputPath);
|
|
510
|
+
if (!import_node_fs.default.existsSync(localesOutputDir)) {
|
|
511
|
+
try {
|
|
512
|
+
import_node_fs.default.mkdirSync(localesOutputDir, { recursive: true });
|
|
513
|
+
} catch (err) {
|
|
514
|
+
throw new Error(`Failed to create directory "${localesOutputDir}"`, { cause: err });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const validLocales = locales.filter((locale) => locale && locale.trim().length > 0);
|
|
518
|
+
const content = `/**
|
|
519
|
+
* This file is auto-generated from the Google Spreadsheet package.
|
|
520
|
+
* It contains the list of available locales and their mappings to original spreadsheet headers.
|
|
521
|
+
* Do not edit this file manually, it will be overwritten by the package.
|
|
522
|
+
*/
|
|
523
|
+
|
|
524
|
+
export const locales = ${JSON.stringify(validLocales)};
|
|
525
|
+
|
|
2
526
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
527
|
+
* Mapping from normalized locale codes to original spreadsheet headers
|
|
528
|
+
* Used for syncing changes back to the spreadsheet
|
|
5
529
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
530
|
+
export const localeHeaderMapping = ${JSON.stringify(localeMapping, null, 2)};
|
|
531
|
+
|
|
532
|
+
export default locales;
|
|
533
|
+
`;
|
|
534
|
+
try {
|
|
535
|
+
import_node_fs.default.writeFileSync(localesOutputPath, content, "utf8");
|
|
536
|
+
} catch (err) {
|
|
537
|
+
throw new Error(`Failed to write locales file at "${localesOutputPath}"`, { cause: err });
|
|
538
|
+
}
|
|
539
|
+
console.log(`Successfully wrote locales file with ${validLocales.length} locales:`, validLocales);
|
|
540
|
+
console.log("Header mapping includes:", Object.keys(localeMapping).length, "mappings");
|
|
541
|
+
}
|
|
542
|
+
function writeLanguageDataFile(translations, locales, dataJsonPath) {
|
|
543
|
+
const dataJsonDir = import_node_path2.default.dirname(dataJsonPath);
|
|
544
|
+
if (!import_node_fs.default.existsSync(dataJsonDir)) {
|
|
545
|
+
try {
|
|
546
|
+
import_node_fs.default.mkdirSync(dataJsonDir, { recursive: true });
|
|
547
|
+
} catch (err) {
|
|
548
|
+
throw new Error(`Failed to create directory "${dataJsonDir}"`, { cause: err });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const dataJsonContent = convertToDataJsonFormat(translations, locales);
|
|
552
|
+
try {
|
|
553
|
+
import_node_fs.default.writeFileSync(
|
|
554
|
+
dataJsonPath,
|
|
555
|
+
JSON.stringify(dataJsonContent, null, 2),
|
|
556
|
+
"utf8"
|
|
557
|
+
);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
throw new Error(`Failed to write language data file at "${dataJsonPath}"`, { cause: err });
|
|
560
|
+
}
|
|
561
|
+
console.log("Successfully updated languageData.json with fresh spreadsheet data");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/utils/dataConverter/findLocalChanges.ts
|
|
565
|
+
function findLocalChanges(localData, spreadsheetData) {
|
|
566
|
+
const changes = {};
|
|
567
|
+
for (const locale of Object.keys(localData)) {
|
|
568
|
+
if (!localData[locale]) continue;
|
|
569
|
+
const resolvedLocale = resolveLocaleWithFallback(locale, Object.keys(spreadsheetData));
|
|
570
|
+
for (const sheet of Object.keys(localData[locale])) {
|
|
571
|
+
if (!localData[locale][sheet]) continue;
|
|
572
|
+
for (const key of Object.keys(localData[locale][sheet])) {
|
|
573
|
+
const isNewKey = !resolvedLocale || !spreadsheetData[resolvedLocale]?.[sheet] || !spreadsheetData[resolvedLocale][sheet][key];
|
|
574
|
+
if (isNewKey) {
|
|
575
|
+
if (!changes[locale]) changes[locale] = {};
|
|
576
|
+
if (!changes[locale][sheet]) changes[locale][sheet] = {};
|
|
577
|
+
changes[locale][sheet][key] = localData[locale][sheet][key];
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return changes;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/utils/spreadsheetUpdater.ts
|
|
586
|
+
function columnIndexToLetter(index) {
|
|
587
|
+
let result = "";
|
|
588
|
+
let i = index;
|
|
589
|
+
do {
|
|
590
|
+
result = String.fromCharCode(65 + i % 26) + result;
|
|
591
|
+
i = Math.floor(i / 26) - 1;
|
|
592
|
+
} while (i >= 0);
|
|
593
|
+
return result;
|
|
594
|
+
}
|
|
595
|
+
async function updateSpreadsheetWithLocalChanges(doc, changes, waitSeconds, autoTranslate = false, localeMapping = {}) {
|
|
596
|
+
console.log("Updating spreadsheet with local changes...");
|
|
597
|
+
const baseDelayMs = waitSeconds * 1e3;
|
|
598
|
+
for (const sheetTitle of new Set(
|
|
599
|
+
Object.values(changes).flatMap((locale) => Object.keys(locale))
|
|
600
|
+
)) {
|
|
601
|
+
console.log(`Processing sheet: ${sheetTitle}`);
|
|
602
|
+
let sheet = doc.sheetsByTitle[sheetTitle];
|
|
603
|
+
if (!sheet) {
|
|
604
|
+
const localeHeaders = Object.values(localeMapping);
|
|
605
|
+
if (localeHeaders.length === 0) {
|
|
606
|
+
console.warn(`Sheet "${sheetTitle}" not found in the document, cannot update`);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
console.log(`Sheet "${sheetTitle}" not found \u2014 creating it with ${localeHeaders.length} locale column(s).`);
|
|
610
|
+
sheet = await withRetry(
|
|
611
|
+
() => doc.addSheet({ title: sheetTitle, headerValues: ["key", ...localeHeaders] }),
|
|
612
|
+
`addSheet: ${sheetTitle}`,
|
|
613
|
+
baseDelayMs
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
if (!sheet) {
|
|
617
|
+
console.warn(`Sheet "${sheetTitle}" could not be found or created, skipping.`);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const rows = await withRetry(
|
|
621
|
+
() => sheet.getRows(),
|
|
622
|
+
`getRows: ${sheetTitle}`,
|
|
623
|
+
baseDelayMs
|
|
624
|
+
);
|
|
625
|
+
let headerRow;
|
|
626
|
+
let originalHeaders;
|
|
627
|
+
if (rows.length > 0) {
|
|
628
|
+
originalHeaders = Object.keys(rows[0].toObject());
|
|
629
|
+
headerRow = originalHeaders.map((h) => h.toLowerCase());
|
|
630
|
+
} else {
|
|
631
|
+
const localeHeaders = Object.values(localeMapping);
|
|
632
|
+
if (localeHeaders.length === 0) {
|
|
633
|
+
console.warn(`No rows found in sheet "${sheetTitle}", cannot update`);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
originalHeaders = ["key", ...localeHeaders];
|
|
637
|
+
headerRow = originalHeaders.map((h) => h.toLowerCase());
|
|
638
|
+
}
|
|
639
|
+
const keyColumn = headerRow[0];
|
|
640
|
+
const locales = headerRow.filter((key) => key !== keyColumn);
|
|
641
|
+
const existingKeys = /* @__PURE__ */ new Map();
|
|
642
|
+
rows.forEach((row, index) => {
|
|
643
|
+
const rowData = row.toObject();
|
|
644
|
+
const keyField = Object.keys(rowData).find((k) => k.toLowerCase() === keyColumn);
|
|
645
|
+
if (keyField && rowData[keyField]) {
|
|
646
|
+
existingKeys.set(rowData[keyField].toString().toLowerCase(), index);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
const newKeys = /* @__PURE__ */ new Map();
|
|
650
|
+
const keyLocalesMap = /* @__PURE__ */ new Map();
|
|
651
|
+
for (const locale of Object.keys(changes)) {
|
|
652
|
+
if (!changes[locale]?.[sheetTitle]) continue;
|
|
653
|
+
const localeData = changes[locale][sheetTitle];
|
|
654
|
+
for (const key of Object.keys(localeData)) {
|
|
655
|
+
const keyLower = key.toLowerCase();
|
|
656
|
+
if (!existingKeys.has(keyLower)) {
|
|
657
|
+
if (!newKeys.has(keyLower)) {
|
|
658
|
+
newKeys.set(keyLower, { [keyColumn]: key });
|
|
659
|
+
keyLocalesMap.set(keyLower, /* @__PURE__ */ new Map());
|
|
660
|
+
}
|
|
661
|
+
let localeHeader = getOriginalHeaderForLocale(locale, localeMapping);
|
|
662
|
+
if (!localeHeader) {
|
|
663
|
+
const localeLang = getLanguagePrefix(locale);
|
|
664
|
+
localeHeader = originalHeaders.find(
|
|
665
|
+
(h) => getLanguagePrefix(h) === localeLang
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
if (localeHeader) {
|
|
669
|
+
const theKey = newKeys.get(keyLower);
|
|
670
|
+
if (!theKey) {
|
|
671
|
+
console.warn(`Key "${key}" not found in newKeys map, skipping...`);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const value = String(localeData[key]);
|
|
675
|
+
theKey[localeHeader] = value;
|
|
676
|
+
const localesForKey = keyLocalesMap.get(keyLower);
|
|
677
|
+
if (localesForKey) {
|
|
678
|
+
localesForKey.set(locale.toLowerCase(), localeHeader);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
const rowIndex = existingKeys.get(keyLower);
|
|
683
|
+
const row = rows[rowIndex];
|
|
684
|
+
let localeHeader = getOriginalHeaderForLocale(locale, localeMapping);
|
|
685
|
+
if (!localeHeader) {
|
|
686
|
+
const localeLang = getLanguagePrefix(locale);
|
|
687
|
+
localeHeader = Object.keys(row.toObject()).find(
|
|
688
|
+
(h) => getLanguagePrefix(h) === localeLang
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
if (localeHeader) {
|
|
692
|
+
row.set(localeHeader, String(localeData[key]));
|
|
693
|
+
try {
|
|
694
|
+
await withRetry(
|
|
695
|
+
() => row.save(),
|
|
696
|
+
`save row ${rowIndex} in ${sheetTitle}`,
|
|
697
|
+
baseDelayMs
|
|
698
|
+
);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
console.error(
|
|
701
|
+
`Failed to save row for key "${keyLower}" in sheet "${sheetTitle}":`,
|
|
702
|
+
err
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (newKeys.size > 0) {
|
|
710
|
+
console.log(`Adding ${newKeys.size} new keys to sheet ${sheetTitle}...`);
|
|
711
|
+
if (autoTranslate) {
|
|
712
|
+
for (const [keyLower, rowData] of newKeys.entries()) {
|
|
713
|
+
const localesWithValues = keyLocalesMap.get(keyLower);
|
|
714
|
+
if (localesWithValues && localesWithValues.size > 0) {
|
|
715
|
+
const [, sourceHeader] = [...localesWithValues.entries()][0];
|
|
716
|
+
for (const localeHeader of locales) {
|
|
717
|
+
const localeLower = localeHeader.toLowerCase();
|
|
718
|
+
const rowDataKey = Object.keys(rowData).find((k) => k.toLowerCase() === localeLower);
|
|
719
|
+
if (localesWithValues.has(localeLower) || rowDataKey && rowData[rowDataKey]) {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const exactHeaderName = headerRow.find(
|
|
723
|
+
(h) => h.toLowerCase() === localeLower
|
|
724
|
+
);
|
|
725
|
+
if (exactHeaderName) {
|
|
726
|
+
const sourceHeaderIndex = headerRow.indexOf(sourceHeader.toLowerCase());
|
|
727
|
+
const targetHeaderIndex = headerRow.indexOf(exactHeaderName);
|
|
728
|
+
if (sourceHeaderIndex < 0 || targetHeaderIndex < 0) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const sourceColumnLetter = columnIndexToLetter(sourceHeaderIndex);
|
|
732
|
+
const targetColumnLetter = columnIndexToLetter(targetHeaderIndex);
|
|
733
|
+
rowData[exactHeaderName] = `=GOOGLETRANSLATE(INDIRECT("${sourceColumnLetter}"&ROW());$${sourceColumnLetter}$1;${targetColumnLetter}$1)`;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const newRows = Array.from(newKeys.values());
|
|
740
|
+
const CHUNK_SIZE = 5;
|
|
741
|
+
for (let i = 0; i < newRows.length; i += CHUNK_SIZE) {
|
|
742
|
+
const chunk = newRows.slice(i, i + CHUNK_SIZE);
|
|
743
|
+
await withRetry(
|
|
744
|
+
() => sheet.addRows(chunk),
|
|
745
|
+
`addRows chunk ${Math.floor(i / CHUNK_SIZE) + 1} in ${sheetTitle}`,
|
|
746
|
+
baseDelayMs
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
console.log("Finished updating spreadsheet with local changes.");
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/utils/isDataJsonNewer.ts
|
|
755
|
+
var import_node_fs3 = __toESM(require("node:fs"));
|
|
756
|
+
var import_node_path3 = __toESM(require("node:path"));
|
|
757
|
+
|
|
758
|
+
// src/utils/getFileLastModified.ts
|
|
759
|
+
var import_node_fs2 = __toESM(require("node:fs"));
|
|
760
|
+
function getFileLastModified(filePath) {
|
|
761
|
+
try {
|
|
762
|
+
const stats = import_node_fs2.default.statSync(filePath);
|
|
763
|
+
return stats.mtime;
|
|
764
|
+
} catch (error) {
|
|
765
|
+
console.warn(`Could not read file stats for "${filePath}":`, error);
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/utils/isDataJsonNewer.ts
|
|
771
|
+
function isDataJsonNewer(dataJsonPath, translationsOutputDir) {
|
|
772
|
+
const dataJsonMtime = getFileLastModified(dataJsonPath);
|
|
773
|
+
if (!dataJsonMtime) return false;
|
|
774
|
+
try {
|
|
775
|
+
const files = import_node_fs3.default.readdirSync(translationsOutputDir).filter((file) => file.endsWith(".json")).map((file) => import_node_path3.default.join(translationsOutputDir, file));
|
|
776
|
+
if (files.length === 0) return true;
|
|
777
|
+
const mostRecentTranslationMtime = files.map((file) => getFileLastModified(file)).filter((d) => d !== null).reduce(
|
|
778
|
+
(max, d) => max === null || d > max ? d : max,
|
|
779
|
+
null
|
|
780
|
+
);
|
|
781
|
+
if (!mostRecentTranslationMtime) return true;
|
|
782
|
+
return dataJsonMtime > mostRecentTranslationMtime;
|
|
783
|
+
} catch (error) {
|
|
784
|
+
console.warn("Error comparing file modification times:", error);
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/utils/readDataJson.ts
|
|
790
|
+
var import_node_fs4 = __toESM(require("node:fs"));
|
|
791
|
+
|
|
792
|
+
// src/utils/dataConverter/convertFromDataJsonFormat.ts
|
|
793
|
+
function isSheetData(v) {
|
|
794
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
795
|
+
}
|
|
796
|
+
function convertFromDataJsonFormat(dataJson) {
|
|
797
|
+
const result = {};
|
|
798
|
+
for (const projectData of dataJson) {
|
|
799
|
+
for (const sheetTitle of Object.keys(projectData)) {
|
|
800
|
+
const raw = projectData[sheetTitle];
|
|
801
|
+
if (!isSheetData(raw)) {
|
|
802
|
+
console.warn(`Skipping malformed entry for sheet "${sheetTitle}"`);
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
for (const locale of Object.keys(raw)) {
|
|
806
|
+
if (!result[locale]) {
|
|
807
|
+
result[locale] = {};
|
|
808
|
+
}
|
|
809
|
+
if (!result[locale][sheetTitle]) {
|
|
810
|
+
result[locale][sheetTitle] = {};
|
|
811
|
+
}
|
|
812
|
+
const localeData = raw[locale];
|
|
813
|
+
if (typeof localeData !== "object" || localeData === null) {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
for (const key of Object.keys(localeData)) {
|
|
817
|
+
result[locale][sheetTitle][key] = localeData[key];
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return result;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/utils/readDataJson.ts
|
|
826
|
+
function readDataJson(dataJsonPath) {
|
|
827
|
+
try {
|
|
828
|
+
if (!import_node_fs4.default.existsSync(dataJsonPath)) {
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
const dataJsonContent = import_node_fs4.default.readFileSync(dataJsonPath, "utf8");
|
|
832
|
+
const dataJson = JSON.parse(dataJsonContent);
|
|
833
|
+
return convertFromDataJsonFormat(dataJson);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
console.warn("Error reading or parsing languageData.json:", error);
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/utils/syncManager.ts
|
|
841
|
+
async function handleBidirectionalSync(doc, dataJsonPath, translationsOutputDir, syncLocalChanges, autoTranslate, spreadsheetData, waitSeconds, localeMapping = {}) {
|
|
842
|
+
const result = {
|
|
843
|
+
shouldRefresh: false,
|
|
844
|
+
hasChanges: false
|
|
845
|
+
};
|
|
846
|
+
const localData = readDataJson(dataJsonPath);
|
|
847
|
+
const dataJsonExists = localData !== null;
|
|
848
|
+
const shouldSyncToSheet = syncLocalChanges && dataJsonExists && isDataJsonNewer(dataJsonPath, translationsOutputDir);
|
|
849
|
+
if (!shouldSyncToSheet || !localData) {
|
|
850
|
+
return result;
|
|
851
|
+
}
|
|
852
|
+
console.log("Local languageData.json is newer than translation files. Checking for changes...");
|
|
853
|
+
const changes = findLocalChanges(localData, spreadsheetData);
|
|
854
|
+
const hasChanges = Object.keys(changes).length > 0 && Object.keys(changes).some(
|
|
855
|
+
(locale) => Object.keys(changes[locale]).length > 0
|
|
856
|
+
);
|
|
857
|
+
if (!hasChanges) {
|
|
858
|
+
console.log("No local changes found that need to be synced to the spreadsheet.");
|
|
859
|
+
return result;
|
|
860
|
+
}
|
|
861
|
+
const localesCount = Object.keys(changes).length;
|
|
862
|
+
const keysCount = Object.values(changes).flatMap((l) => Object.values(l)).flatMap((s) => Object.keys(s)).length;
|
|
863
|
+
console.log(`Found local changes: ${localesCount} locale(s), ~${keysCount} key(s) to sync to the spreadsheet.`);
|
|
864
|
+
try {
|
|
865
|
+
await updateSpreadsheetWithLocalChanges(doc, changes, waitSeconds, autoTranslate, localeMapping);
|
|
866
|
+
result.shouldRefresh = true;
|
|
867
|
+
result.hasChanges = true;
|
|
868
|
+
} catch (err) {
|
|
869
|
+
console.error("Failed to sync local changes to spreadsheet:", err);
|
|
870
|
+
}
|
|
871
|
+
return result;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// src/utils/publicSheetReader.ts
|
|
875
|
+
var import_node_https = __toESM(require("node:https"));
|
|
876
|
+
var import_node_http = __toESM(require("node:http"));
|
|
877
|
+
function fetchUrl(url) {
|
|
878
|
+
return new Promise((resolve, reject) => {
|
|
879
|
+
const client = url.startsWith("https") ? import_node_https.default : import_node_http.default;
|
|
880
|
+
const req = client.get(url, (res) => {
|
|
881
|
+
if (res.statusCode !== void 0 && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
882
|
+
fetchUrl(res.headers.location).then(resolve).catch(reject);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (res.statusCode !== void 0 && res.statusCode >= 400) {
|
|
886
|
+
reject(new Error(`HTTP ${res.statusCode} while fetching ${url}`));
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
let data = "";
|
|
890
|
+
res.on("data", (chunk) => {
|
|
891
|
+
data += chunk.toString();
|
|
892
|
+
});
|
|
893
|
+
res.on("end", () => resolve(data));
|
|
894
|
+
res.on("error", reject);
|
|
895
|
+
});
|
|
896
|
+
req.on("error", reject);
|
|
897
|
+
req.end();
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
function parseGvizResponse(raw) {
|
|
901
|
+
const match = raw.match(/google\.visualization\.Query\.setResponse\((\{[\s\S]*?\})\)/);
|
|
902
|
+
if (!match) {
|
|
903
|
+
throw new Error(
|
|
904
|
+
'Unexpected response format from Google Visualization API. Make sure the spreadsheet is shared as "Anyone with link can view".'
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
return JSON.parse(match[1]);
|
|
908
|
+
}
|
|
909
|
+
async function readPublicSheet(spreadsheetId, sheetName) {
|
|
910
|
+
const url = `https://docs.google.com/spreadsheets/d/${encodeURIComponent(spreadsheetId)}/gviz/tq?tqx=out:json&headers=1&sheet=${encodeURIComponent(sheetName)}`;
|
|
911
|
+
let raw;
|
|
912
|
+
try {
|
|
913
|
+
raw = await fetchUrl(url);
|
|
914
|
+
} catch (err) {
|
|
915
|
+
throw new Error(
|
|
916
|
+
`Failed to fetch public sheet "${sheetName}" from spreadsheet "${spreadsheetId}"`,
|
|
917
|
+
{ cause: err }
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
let data;
|
|
921
|
+
try {
|
|
922
|
+
data = parseGvizResponse(raw);
|
|
923
|
+
} catch (err) {
|
|
924
|
+
throw new Error(
|
|
925
|
+
`Failed to parse response for sheet "${sheetName}" in spreadsheet "${spreadsheetId}"`,
|
|
926
|
+
{ cause: err }
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
if (data.status !== "ok") {
|
|
930
|
+
const message = data.errors?.[0]?.message ?? "Unknown error";
|
|
931
|
+
throw new Error(
|
|
932
|
+
`Google Visualization API returned an error for sheet "${sheetName}": ${message}`
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
if (!data.table) {
|
|
936
|
+
return [];
|
|
937
|
+
}
|
|
938
|
+
const { cols, rows } = data.table;
|
|
939
|
+
const headers = cols.map((col) => col.label || col.id);
|
|
940
|
+
return rows.filter((row) => row && row.c).map((row) => {
|
|
941
|
+
const obj = {};
|
|
942
|
+
for (let i = 0; i < headers.length; i++) {
|
|
943
|
+
const cell = row.c?.[i];
|
|
944
|
+
obj[headers[i]] = cell?.v != null ? String(cell.v) : "";
|
|
945
|
+
}
|
|
946
|
+
return obj;
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/utils/spreadsheetCreator.ts
|
|
951
|
+
var import_google_spreadsheet = require("google-spreadsheet");
|
|
952
|
+
function colLetter(index) {
|
|
953
|
+
let result = "";
|
|
954
|
+
let i = index;
|
|
955
|
+
do {
|
|
956
|
+
result = String.fromCharCode(65 + i % 26) + result;
|
|
957
|
+
i = Math.floor(i / 26) - 1;
|
|
958
|
+
} while (i >= 0);
|
|
959
|
+
return result;
|
|
960
|
+
}
|
|
961
|
+
var DEFAULT_TARGET_LOCALES = ["de", "fr", "es", "it", "pt", "ja", "zh"];
|
|
962
|
+
var STARTER_KEYS = {
|
|
963
|
+
"app.name": "My App",
|
|
964
|
+
"app.description": "A great application",
|
|
965
|
+
"nav.home": "Home",
|
|
966
|
+
"nav.about": "About",
|
|
967
|
+
"nav.contact": "Contact",
|
|
968
|
+
"common.save": "Save",
|
|
969
|
+
"common.cancel": "Cancel",
|
|
970
|
+
"common.loading": "Loading\u2026",
|
|
971
|
+
"common.error": "An error occurred",
|
|
972
|
+
"common.success": "Success!"
|
|
973
|
+
};
|
|
974
|
+
async function createSpreadsheet(authClient, options = {}) {
|
|
975
|
+
const {
|
|
976
|
+
title = "google-sheet-translations",
|
|
977
|
+
sourceLocale = "en",
|
|
978
|
+
targetLocales = DEFAULT_TARGET_LOCALES,
|
|
979
|
+
seedKeys = STARTER_KEYS
|
|
980
|
+
} = options;
|
|
981
|
+
await authClient.authorize();
|
|
982
|
+
const createRes = await withRetry(
|
|
983
|
+
() => authClient.request({
|
|
984
|
+
url: "https://sheets.googleapis.com/v4/spreadsheets",
|
|
985
|
+
method: "POST",
|
|
986
|
+
data: {
|
|
987
|
+
properties: { title },
|
|
988
|
+
sheets: [
|
|
989
|
+
{ properties: { title: "__welcome__", index: 0 } },
|
|
990
|
+
{ properties: { title: "i18n", index: 1 } }
|
|
991
|
+
]
|
|
992
|
+
}
|
|
993
|
+
}),
|
|
994
|
+
"createSpreadsheet"
|
|
995
|
+
);
|
|
996
|
+
const spreadsheetId = createRes.data.spreadsheetId;
|
|
997
|
+
const url = `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit`;
|
|
998
|
+
const doc = new import_google_spreadsheet.GoogleSpreadsheet(spreadsheetId, authClient);
|
|
999
|
+
await withRetry(() => doc.loadInfo(), "loadInfo after create");
|
|
1000
|
+
const welcomeSheet = doc.sheetsByTitle["__welcome__"];
|
|
1001
|
+
if (welcomeSheet) {
|
|
1002
|
+
await withRetry(
|
|
1003
|
+
() => welcomeSheet.loadCells("A1:B20"),
|
|
1004
|
+
"loadCells welcome"
|
|
1005
|
+
);
|
|
1006
|
+
const lines = [
|
|
1007
|
+
["\u{1F4CA} Google Sheet Translations", ""],
|
|
1008
|
+
["", ""],
|
|
1009
|
+
["Package:", "@el-j/google-sheet-translations"],
|
|
1010
|
+
["Docs:", "https://el-j.github.io/google-sheet-translations/"],
|
|
1011
|
+
["", ""],
|
|
1012
|
+
["Your Spreadsheet ID:", spreadsheetId],
|
|
1013
|
+
["URL:", url],
|
|
1014
|
+
["", ""],
|
|
1015
|
+
["Add to your .env file:", ""],
|
|
1016
|
+
["GOOGLE_SPREADSHEET_ID=" + spreadsheetId, ""],
|
|
1017
|
+
["", ""],
|
|
1018
|
+
["How this works:", ""],
|
|
1019
|
+
['1. The "i18n" sheet (and any other sheet you add) holds your translation keys.', ""],
|
|
1020
|
+
["2. Column A = key, Column B = source language, other columns = auto-translated.", ""],
|
|
1021
|
+
["3. Run getSpreadSheetData(['i18n']) in your project to sync to local files.", ""],
|
|
1022
|
+
["4. Add more sheets for different pages/features of your app.", ""],
|
|
1023
|
+
["5. Use syncLocalChanges: true (default) to push new keys back to this spreadsheet.", ""]
|
|
1024
|
+
];
|
|
1025
|
+
for (let r = 0; r < lines.length; r++) {
|
|
1026
|
+
for (let c = 0; c < lines[r].length; c++) {
|
|
1027
|
+
const cell = welcomeSheet.getCell(r, c);
|
|
1028
|
+
cell.value = lines[r][c];
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
await withRetry(() => welcomeSheet.saveUpdatedCells(), "saveWelcome");
|
|
1032
|
+
}
|
|
1033
|
+
const i18nSheet = doc.sheetsByTitle["i18n"];
|
|
1034
|
+
if (i18nSheet) {
|
|
1035
|
+
const allLocales = [sourceLocale, ...targetLocales];
|
|
1036
|
+
await withRetry(
|
|
1037
|
+
() => i18nSheet.setHeaderRow(["key", ...allLocales]),
|
|
1038
|
+
"setHeaderRow"
|
|
1039
|
+
);
|
|
1040
|
+
const sourceColLetter = colLetter(1);
|
|
1041
|
+
const rows = Object.entries(seedKeys).map(([key, sourceValue]) => {
|
|
1042
|
+
const row = { key, [sourceLocale]: sourceValue };
|
|
1043
|
+
targetLocales.forEach((locale, idx) => {
|
|
1044
|
+
const targetColLetter = colLetter(2 + idx);
|
|
1045
|
+
row[locale] = `=GOOGLETRANSLATE(INDIRECT("${sourceColLetter}"&ROW());$${sourceColLetter}$1;${targetColLetter}$1)`;
|
|
1046
|
+
});
|
|
1047
|
+
return row;
|
|
1048
|
+
});
|
|
1049
|
+
await withRetry(() => i18nSheet.addRows(rows), "addRows i18n");
|
|
1050
|
+
}
|
|
1051
|
+
console.log("");
|
|
1052
|
+
console.log("\u2705 New spreadsheet created!");
|
|
1053
|
+
console.log(` Title : ${title}`);
|
|
1054
|
+
console.log(` URL : ${url}`);
|
|
1055
|
+
console.log(` ID : ${spreadsheetId}`);
|
|
1056
|
+
console.log("");
|
|
1057
|
+
console.log(" Add this to your .env file (or environment):");
|
|
1058
|
+
console.log(` GOOGLE_SPREADSHEET_ID=${spreadsheetId}`);
|
|
1059
|
+
console.log("");
|
|
1060
|
+
return { spreadsheetId, url };
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// src/getSpreadSheetData.ts
|
|
1064
|
+
var MAX_SYNC_REFRESH_DEPTH = 1;
|
|
1065
|
+
async function persistSpreadsheetId(id) {
|
|
1066
|
+
const envPath = import_node_path4.default.join(process.cwd(), ".env");
|
|
1067
|
+
try {
|
|
1068
|
+
let content = "";
|
|
1069
|
+
if (import_node_fs5.default.existsSync(envPath)) {
|
|
1070
|
+
content = import_node_fs5.default.readFileSync(envPath, "utf8");
|
|
1071
|
+
if (/^GOOGLE_SPREADSHEET_ID=/m.test(content)) {
|
|
1072
|
+
content = content.replace(/^GOOGLE_SPREADSHEET_ID=.*/m, `GOOGLE_SPREADSHEET_ID=${id}`);
|
|
1073
|
+
} else {
|
|
1074
|
+
content = content.trimEnd() + `
|
|
1075
|
+
GOOGLE_SPREADSHEET_ID=${id}
|
|
1076
|
+
`;
|
|
1077
|
+
}
|
|
1078
|
+
} else {
|
|
1079
|
+
content = `GOOGLE_SPREADSHEET_ID=${id}
|
|
1080
|
+
`;
|
|
1081
|
+
}
|
|
1082
|
+
import_node_fs5.default.writeFileSync(envPath, content, "utf8");
|
|
1083
|
+
console.log(` Saved GOOGLE_SPREADSHEET_ID to ${envPath}`);
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
console.warn(` Could not write .env: ${err.message}`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
async function getSpreadSheetData(_docTitle, options = {}, _refreshDepth = 0) {
|
|
1089
|
+
const config = normalizeConfig(options);
|
|
1090
|
+
const baseDelayMs = config.waitSeconds * 1e3;
|
|
1091
|
+
const docTitle = _docTitle ?? [];
|
|
1092
|
+
if (docTitle.length === 0) {
|
|
1093
|
+
console.warn("No sheet titles provided, cannot process spreadsheet data");
|
|
1094
|
+
return {};
|
|
1095
|
+
}
|
|
1096
|
+
if (!docTitle.includes("i18n")) {
|
|
1097
|
+
docTitle.push("i18n");
|
|
1098
|
+
}
|
|
1099
|
+
const finalTranslations = {};
|
|
1100
|
+
const allLocales = /* @__PURE__ */ new Set();
|
|
1101
|
+
const localesWithContent = /* @__PURE__ */ new Set();
|
|
1102
|
+
const globalLocaleMapping = {};
|
|
1103
|
+
const globalOriginalMapping = {};
|
|
1104
|
+
function mergeResult(result, title) {
|
|
1105
|
+
if (!result.success) return;
|
|
1106
|
+
for (const [normalized, original] of Object.entries(result.localeMapping)) {
|
|
1107
|
+
if (!globalLocaleMapping[normalized]) globalLocaleMapping[normalized] = original;
|
|
1108
|
+
}
|
|
1109
|
+
for (const [original, normalized] of Object.entries(result.originalMapping)) {
|
|
1110
|
+
if (!globalOriginalMapping[original]) globalOriginalMapping[original] = normalized;
|
|
1111
|
+
}
|
|
1112
|
+
for (const locale of result.locales) {
|
|
1113
|
+
if (finalTranslations[locale]) {
|
|
1114
|
+
finalTranslations[locale] = { ...finalTranslations[locale], ...result.translations[locale] };
|
|
1115
|
+
} else {
|
|
1116
|
+
finalTranslations[locale] = result.translations[locale];
|
|
1117
|
+
}
|
|
1118
|
+
allLocales.add(locale);
|
|
1119
|
+
if (title !== "i18n" && result.translations[locale]) {
|
|
1120
|
+
const hasActualTranslations = Object.values(result.translations[locale]).some(
|
|
1121
|
+
(sheetTranslations) => Object.keys(sheetTranslations).length > 0
|
|
1122
|
+
);
|
|
1123
|
+
if (hasActualTranslations) localesWithContent.add(locale);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
if (config.publicSheet) {
|
|
1128
|
+
const spreadsheetId = config.spreadsheetId ?? process.env.GOOGLE_SPREADSHEET_ID;
|
|
1129
|
+
if (!spreadsheetId) {
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
"No spreadsheet ID provided. Set GOOGLE_SPREADSHEET_ID or pass spreadsheetId in options."
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
console.log(`Processing ${docTitle.length} sheets: ${docTitle.join(", ")}`);
|
|
1135
|
+
await Promise.all(
|
|
1136
|
+
docTitle.map(async (title) => {
|
|
1137
|
+
let rows;
|
|
1138
|
+
try {
|
|
1139
|
+
rows = await withRetry(
|
|
1140
|
+
() => readPublicSheet(spreadsheetId, title),
|
|
1141
|
+
`readPublicSheet: ${title}`,
|
|
1142
|
+
baseDelayMs
|
|
1143
|
+
);
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
console.warn(`Sheet "${title}" could not be fetched: ${err.message}`);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
mergeResult(await processRawRows(rows, title), title);
|
|
1149
|
+
})
|
|
1150
|
+
);
|
|
1151
|
+
} else {
|
|
1152
|
+
const serviceAuthClient = createAuthClient();
|
|
1153
|
+
let spreadsheetId = config.spreadsheetId ?? process.env.GOOGLE_SPREADSHEET_ID;
|
|
1154
|
+
if (!spreadsheetId) {
|
|
1155
|
+
if (config.autoCreate) {
|
|
1156
|
+
const created = await createSpreadsheet(serviceAuthClient, {
|
|
1157
|
+
title: config.spreadsheetTitle,
|
|
1158
|
+
sourceLocale: config.sourceLocale,
|
|
1159
|
+
targetLocales: config.targetLocales
|
|
1160
|
+
});
|
|
1161
|
+
spreadsheetId = created.spreadsheetId;
|
|
1162
|
+
await persistSpreadsheetId(spreadsheetId);
|
|
1163
|
+
} else {
|
|
1164
|
+
throw new Error(
|
|
1165
|
+
"No spreadsheet ID provided. Set GOOGLE_SPREADSHEET_ID or pass spreadsheetId in options."
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
console.log(`Processing ${docTitle.length} sheets: ${docTitle.join(", ")}`);
|
|
1170
|
+
const doc = new import_google_spreadsheet2.GoogleSpreadsheet(spreadsheetId, serviceAuthClient);
|
|
1171
|
+
try {
|
|
1172
|
+
await withRetry(() => doc.loadInfo(true), "loadInfo", baseDelayMs);
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
throw new Error(`Failed to load spreadsheet "${spreadsheetId}"`, { cause: err });
|
|
1175
|
+
}
|
|
1176
|
+
await Promise.all(
|
|
1177
|
+
docTitle.map(async (title) => {
|
|
1178
|
+
const sheet = doc.sheetsByTitle[title];
|
|
1179
|
+
if (!sheet) {
|
|
1180
|
+
console.warn(`Sheet "${title}" not found in the document`);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
mergeResult(await processSheet(sheet, title, config.rowLimit, baseDelayMs), title);
|
|
1184
|
+
})
|
|
1185
|
+
);
|
|
1186
|
+
const syncResult = await handleBidirectionalSync(
|
|
1187
|
+
doc,
|
|
1188
|
+
config.dataJsonPath,
|
|
1189
|
+
config.translationsOutputDir,
|
|
1190
|
+
config.syncLocalChanges,
|
|
1191
|
+
config.autoTranslate,
|
|
1192
|
+
finalTranslations,
|
|
1193
|
+
config.waitSeconds,
|
|
1194
|
+
globalLocaleMapping
|
|
1195
|
+
);
|
|
1196
|
+
if (syncResult.shouldRefresh && _refreshDepth < MAX_SYNC_REFRESH_DEPTH) {
|
|
1197
|
+
return getSpreadSheetData(
|
|
1198
|
+
_docTitle,
|
|
1199
|
+
{ ...options, syncLocalChanges: false },
|
|
1200
|
+
_refreshDepth + 1
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
const localesForOutput = localesWithContent.size > 0 ? Array.from(localesWithContent) : Array.from(allLocales);
|
|
1205
|
+
const allLocalesArray = Array.from(allLocales);
|
|
1206
|
+
writeTranslationFiles(finalTranslations, allLocalesArray, config.translationsOutputDir);
|
|
1207
|
+
writeLocalesFile(localesForOutput, globalLocaleMapping, config.localesOutputPath);
|
|
1208
|
+
console.log(
|
|
1209
|
+
`Writing locales file with ${localesForOutput.length} locales that have actual translations:`,
|
|
1210
|
+
localesForOutput
|
|
1211
|
+
);
|
|
1212
|
+
if (Object.keys(finalTranslations).length > 0) {
|
|
1213
|
+
writeLanguageDataFile(finalTranslations, allLocalesArray, config.dataJsonPath);
|
|
1214
|
+
}
|
|
1215
|
+
return finalTranslations;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// src/utils/wait.ts
|
|
1219
|
+
var import_promises2 = require("node:timers/promises");
|
|
1220
|
+
function wait(seconds, reason) {
|
|
1221
|
+
console.log("wait", seconds, reason);
|
|
1222
|
+
return (0, import_promises2.setTimeout)(seconds * 1e3);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/utils/translationHelpers.ts
|
|
1226
|
+
function getTranslationSummary(translations) {
|
|
1227
|
+
const summary = {};
|
|
1228
|
+
for (const locale of Object.keys(translations)) {
|
|
1229
|
+
summary[locale] = Object.entries(translations[locale]).map(([sheet, keys]) => ({
|
|
1230
|
+
sheet,
|
|
1231
|
+
count: Object.keys(keys).length
|
|
1232
|
+
}));
|
|
1233
|
+
}
|
|
1234
|
+
return summary;
|
|
1235
|
+
}
|
|
1236
|
+
function getLocaleDisplayName(locale, translations, i18nSheet = "i18n") {
|
|
1237
|
+
const localeData = translations[locale];
|
|
1238
|
+
if (!localeData) return locale;
|
|
1239
|
+
const sheetData = localeData[i18nSheet];
|
|
1240
|
+
if (!sheetData) return locale;
|
|
1241
|
+
const name = sheetData[locale] ?? sheetData[locale.toLowerCase()];
|
|
1242
|
+
return typeof name === "string" ? name : locale;
|
|
1243
|
+
}
|
|
1244
|
+
function mergeSheets(translations, locale, sheetNames) {
|
|
1245
|
+
const localeData = translations[locale];
|
|
1246
|
+
if (!localeData) return {};
|
|
1247
|
+
const sheets = sheetNames ?? Object.keys(localeData);
|
|
1248
|
+
const merged = {};
|
|
1249
|
+
for (const sheetName of sheets) {
|
|
1250
|
+
const sheetData = localeData[sheetName];
|
|
1251
|
+
if (sheetData) {
|
|
1252
|
+
Object.assign(merged, sheetData);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return merged;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// src/index.ts
|
|
1259
|
+
var index_default = getSpreadSheetData;
|
|
1260
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1261
|
+
0 && (module.exports = {
|
|
1262
|
+
DEFAULT_WAIT_SECONDS,
|
|
1263
|
+
convertFromDataJsonFormat,
|
|
1264
|
+
convertToDataJsonFormat,
|
|
1265
|
+
createAuthClient,
|
|
1266
|
+
createLocaleMapping,
|
|
1267
|
+
createSpreadsheet,
|
|
1268
|
+
filterValidLocales,
|
|
1269
|
+
findLocalChanges,
|
|
1270
|
+
getLanguagePrefix,
|
|
1271
|
+
getLocaleDisplayName,
|
|
1272
|
+
getNormalizedLocaleForHeader,
|
|
1273
|
+
getOriginalHeaderForLocale,
|
|
1274
|
+
getSpreadSheetData,
|
|
1275
|
+
getTranslationSummary,
|
|
1276
|
+
handleBidirectionalSync,
|
|
1277
|
+
isValidLocale,
|
|
1278
|
+
mergeSheets,
|
|
1279
|
+
normalizeLocaleCode,
|
|
1280
|
+
processRawRows,
|
|
1281
|
+
readPublicSheet,
|
|
1282
|
+
resolveLocaleWithFallback,
|
|
1283
|
+
updateSpreadsheetWithLocalChanges,
|
|
1284
|
+
validateCredentials,
|
|
1285
|
+
validateEnv,
|
|
1286
|
+
wait,
|
|
1287
|
+
withRetry,
|
|
1288
|
+
writeLanguageDataFile,
|
|
1289
|
+
writeLocalesFile,
|
|
1290
|
+
writeTranslationFiles
|
|
1291
|
+
});
|