@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.
Files changed (60) hide show
  1. package/README.md +58 -55
  2. package/dist/action-entrypoint.d.ts +2 -0
  3. package/dist/action-entrypoint.d.ts.map +1 -0
  4. package/dist/esm/index.js +1226 -0
  5. package/dist/esm/package.json +1 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1288 -41
  9. package/dist/utils/dataConverter/findLocalChanges.d.ts +2 -1
  10. package/dist/utils/dataConverter/findLocalChanges.d.ts.map +1 -1
  11. package/dist/utils/localeNormalizer.d.ts +31 -0
  12. package/dist/utils/localeNormalizer.d.ts.map +1 -1
  13. package/dist/utils/translationHelpers.d.ts +107 -0
  14. package/dist/utils/translationHelpers.d.ts.map +1 -0
  15. package/package.json +21 -14
  16. package/dist/constants.js +0 -9
  17. package/dist/constants.js.map +0 -1
  18. package/dist/getSpreadSheetData.js +0 -170
  19. package/dist/getSpreadSheetData.js.map +0 -1
  20. package/dist/index.js.map +0 -1
  21. package/dist/types.js +0 -3
  22. package/dist/types.js.map +0 -1
  23. package/dist/utils/auth.js +0 -23
  24. package/dist/utils/auth.js.map +0 -1
  25. package/dist/utils/configurationHandler.js +0 -29
  26. package/dist/utils/configurationHandler.js.map +0 -1
  27. package/dist/utils/dataConverter/convertFromDataJsonFormat.js +0 -47
  28. package/dist/utils/dataConverter/convertFromDataJsonFormat.js.map +0 -1
  29. package/dist/utils/dataConverter/convertToDataJsonFormat.js +0 -51
  30. package/dist/utils/dataConverter/convertToDataJsonFormat.js.map +0 -1
  31. package/dist/utils/dataConverter/findLocalChanges.js +0 -70
  32. package/dist/utils/dataConverter/findLocalChanges.js.map +0 -1
  33. package/dist/utils/fileWriter.js +0 -119
  34. package/dist/utils/fileWriter.js.map +0 -1
  35. package/dist/utils/getFileLastModified.js +0 -23
  36. package/dist/utils/getFileLastModified.js.map +0 -1
  37. package/dist/utils/isDataJsonNewer.js +0 -40
  38. package/dist/utils/isDataJsonNewer.js.map +0 -1
  39. package/dist/utils/localeFilter.js +0 -49
  40. package/dist/utils/localeFilter.js.map +0 -1
  41. package/dist/utils/localeNormalizer.js +0 -176
  42. package/dist/utils/localeNormalizer.js.map +0 -1
  43. package/dist/utils/publicSheetReader.js +0 -109
  44. package/dist/utils/publicSheetReader.js.map +0 -1
  45. package/dist/utils/rateLimiter.js +0 -55
  46. package/dist/utils/rateLimiter.js.map +0 -1
  47. package/dist/utils/readDataJson.js +0 -29
  48. package/dist/utils/readDataJson.js.map +0 -1
  49. package/dist/utils/sheetProcessor.js +0 -121
  50. package/dist/utils/sheetProcessor.js.map +0 -1
  51. package/dist/utils/spreadsheetCreator.js +0 -121
  52. package/dist/utils/spreadsheetCreator.js.map +0 -1
  53. package/dist/utils/spreadsheetUpdater.js +0 -227
  54. package/dist/utils/spreadsheetUpdater.js.map +0 -1
  55. package/dist/utils/syncManager.js +0 -62
  56. package/dist/utils/syncManager.js.map +0 -1
  57. package/dist/utils/validateEnv.js +0 -41
  58. package/dist/utils/validateEnv.js.map +0 -1
  59. package/dist/utils/wait.js +0 -19
  60. 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
- * Google Sheet Translation Manager Package
4
- * Public API surface see package.json for current version
527
+ * Mapping from normalized locale codes to original spreadsheet headers
528
+ * Used for syncing changes back to the spreadsheet
5
529
  */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.filterValidLocales = exports.isValidLocale = exports.validateCredentials = exports.createSpreadsheet = exports.readPublicSheet = exports.updateSpreadsheetWithLocalChanges = exports.findLocalChanges = exports.convertFromDataJsonFormat = exports.convertToDataJsonFormat = exports.createAuthClient = exports.validateEnv = exports.withRetry = exports.wait = exports.DEFAULT_WAIT_SECONDS = exports.getSpreadSheetData = void 0;
8
- // Main entry point
9
- var getSpreadSheetData_1 = require("./getSpreadSheetData");
10
- Object.defineProperty(exports, "getSpreadSheetData", { enumerable: true, get: function () { return getSpreadSheetData_1.getSpreadSheetData; } });
11
- Object.defineProperty(exports, "DEFAULT_WAIT_SECONDS", { enumerable: true, get: function () { return getSpreadSheetData_1.DEFAULT_WAIT_SECONDS; } });
12
- // Utility functions required by dependents
13
- var wait_1 = require("./utils/wait");
14
- Object.defineProperty(exports, "wait", { enumerable: true, get: function () { return wait_1.wait; } });
15
- var rateLimiter_1 = require("./utils/rateLimiter");
16
- Object.defineProperty(exports, "withRetry", { enumerable: true, get: function () { return rateLimiter_1.withRetry; } });
17
- var validateEnv_1 = require("./utils/validateEnv");
18
- Object.defineProperty(exports, "validateEnv", { enumerable: true, get: function () { return validateEnv_1.validateEnv; } });
19
- var auth_1 = require("./utils/auth");
20
- Object.defineProperty(exports, "createAuthClient", { enumerable: true, get: function () { return auth_1.createAuthClient; } });
21
- var convertToDataJsonFormat_1 = require("./utils/dataConverter/convertToDataJsonFormat");
22
- Object.defineProperty(exports, "convertToDataJsonFormat", { enumerable: true, get: function () { return convertToDataJsonFormat_1.convertToDataJsonFormat; } });
23
- var convertFromDataJsonFormat_1 = require("./utils/dataConverter/convertFromDataJsonFormat");
24
- Object.defineProperty(exports, "convertFromDataJsonFormat", { enumerable: true, get: function () { return convertFromDataJsonFormat_1.convertFromDataJsonFormat; } });
25
- var findLocalChanges_1 = require("./utils/dataConverter/findLocalChanges");
26
- Object.defineProperty(exports, "findLocalChanges", { enumerable: true, get: function () { return findLocalChanges_1.findLocalChanges; } });
27
- var spreadsheetUpdater_1 = require("./utils/spreadsheetUpdater");
28
- Object.defineProperty(exports, "updateSpreadsheetWithLocalChanges", { enumerable: true, get: function () { return spreadsheetUpdater_1.updateSpreadsheetWithLocalChanges; } });
29
- // Public (unauthenticated) sheet reader
30
- var publicSheetReader_1 = require("./utils/publicSheetReader");
31
- Object.defineProperty(exports, "readPublicSheet", { enumerable: true, get: function () { return publicSheetReader_1.readPublicSheet; } });
32
- // Auto-create spreadsheet utility
33
- var spreadsheetCreator_1 = require("./utils/spreadsheetCreator");
34
- Object.defineProperty(exports, "createSpreadsheet", { enumerable: true, get: function () { return spreadsheetCreator_1.createSpreadsheet; } });
35
- var validateEnv_2 = require("./utils/validateEnv");
36
- Object.defineProperty(exports, "validateCredentials", { enumerable: true, get: function () { return validateEnv_2.validateCredentials; } });
37
- // Locale validation utilities (useful standalone)
38
- var localeFilter_1 = require("./utils/localeFilter");
39
- Object.defineProperty(exports, "isValidLocale", { enumerable: true, get: function () { return localeFilter_1.isValidLocale; } });
40
- Object.defineProperty(exports, "filterValidLocales", { enumerable: true, get: function () { return localeFilter_1.filterValidLocales; } });
41
- // Default export
42
- const getSpreadSheetData_2 = require("./getSpreadSheetData");
43
- exports.default = getSpreadSheetData_2.getSpreadSheetData;
44
- //# sourceMappingURL=index.js.map
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
+ });