gsheets-i18n 1.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 (68) hide show
  1. package/README.md +414 -0
  2. package/dist/cjs/cli/index.js +125 -0
  3. package/dist/cjs/cli/index.js.map +1 -0
  4. package/dist/cjs/index.js +57 -0
  5. package/dist/cjs/index.js.map +1 -0
  6. package/dist/cjs/lib/auth.js +34 -0
  7. package/dist/cjs/lib/auth.js.map +1 -0
  8. package/dist/cjs/lib/defaultKeyMap.js +106 -0
  9. package/dist/cjs/lib/defaultKeyMap.js.map +1 -0
  10. package/dist/cjs/lib/driveClient.js +50 -0
  11. package/dist/cjs/lib/driveClient.js.map +1 -0
  12. package/dist/cjs/lib/folderExtractor.js +63 -0
  13. package/dist/cjs/lib/folderExtractor.js.map +1 -0
  14. package/dist/cjs/lib/parser.js +120 -0
  15. package/dist/cjs/lib/parser.js.map +1 -0
  16. package/dist/cjs/lib/sheetExtractor.js +52 -0
  17. package/dist/cjs/lib/sheetExtractor.js.map +1 -0
  18. package/dist/cjs/lib/sheetsClient.js +37 -0
  19. package/dist/cjs/lib/sheetsClient.js.map +1 -0
  20. package/dist/cjs/lib/writer.js +33 -0
  21. package/dist/cjs/lib/writer.js.map +1 -0
  22. package/dist/cjs/types/index.js +4 -0
  23. package/dist/cjs/types/index.js.map +1 -0
  24. package/dist/esm/cli/index.js +123 -0
  25. package/dist/esm/cli/index.js.map +1 -0
  26. package/dist/esm/index.js +54 -0
  27. package/dist/esm/index.js.map +1 -0
  28. package/dist/esm/lib/auth.js +31 -0
  29. package/dist/esm/lib/auth.js.map +1 -0
  30. package/dist/esm/lib/defaultKeyMap.js +103 -0
  31. package/dist/esm/lib/defaultKeyMap.js.map +1 -0
  32. package/dist/esm/lib/driveClient.js +43 -0
  33. package/dist/esm/lib/driveClient.js.map +1 -0
  34. package/dist/esm/lib/folderExtractor.js +60 -0
  35. package/dist/esm/lib/folderExtractor.js.map +1 -0
  36. package/dist/esm/lib/parser.js +114 -0
  37. package/dist/esm/lib/parser.js.map +1 -0
  38. package/dist/esm/lib/sheetExtractor.js +49 -0
  39. package/dist/esm/lib/sheetExtractor.js.map +1 -0
  40. package/dist/esm/lib/sheetsClient.js +33 -0
  41. package/dist/esm/lib/sheetsClient.js.map +1 -0
  42. package/dist/esm/lib/writer.js +30 -0
  43. package/dist/esm/lib/writer.js.map +1 -0
  44. package/dist/esm/types/index.js +3 -0
  45. package/dist/esm/types/index.js.map +1 -0
  46. package/dist/types/cli/index.d.ts +3 -0
  47. package/dist/types/cli/index.d.ts.map +1 -0
  48. package/dist/types/index.d.ts +21 -0
  49. package/dist/types/index.d.ts.map +1 -0
  50. package/dist/types/lib/auth.d.ts +9 -0
  51. package/dist/types/lib/auth.d.ts.map +1 -0
  52. package/dist/types/lib/defaultKeyMap.d.ts +9 -0
  53. package/dist/types/lib/defaultKeyMap.d.ts.map +1 -0
  54. package/dist/types/lib/driveClient.d.ts +20 -0
  55. package/dist/types/lib/driveClient.d.ts.map +1 -0
  56. package/dist/types/lib/folderExtractor.d.ts +23 -0
  57. package/dist/types/lib/folderExtractor.d.ts.map +1 -0
  58. package/dist/types/lib/parser.d.ts +60 -0
  59. package/dist/types/lib/parser.d.ts.map +1 -0
  60. package/dist/types/lib/sheetExtractor.d.ts +26 -0
  61. package/dist/types/lib/sheetExtractor.d.ts.map +1 -0
  62. package/dist/types/lib/sheetsClient.d.ts +15 -0
  63. package/dist/types/lib/sheetsClient.d.ts.map +1 -0
  64. package/dist/types/lib/writer.d.ts +9 -0
  65. package/dist/types/lib/writer.d.ts.map +1 -0
  66. package/dist/types/types/index.d.ts +153 -0
  67. package/dist/types/types/index.d.ts.map +1 -0
  68. package/package.json +62 -0
package/README.md ADDED
@@ -0,0 +1,414 @@
1
+ # gsheets-i18n
2
+
3
+ Generate i18n JSON files from a Google Spreadsheet — as a CLI tool or a Node.js library.
4
+
5
+ Each tab of your spreadsheet becomes a **namespace**, each row a **translation key**, and each language column an output file. The result is one `<locale>.json` file per language, with nested keys, ready to drop into React i18next, Vue i18n, or any similar framework.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [How it works](#how-it-works)
12
+ - [Installation](#installation)
13
+ - [Google Setup](#google-setup)
14
+ - [Spreadsheet Format](#spreadsheet-format)
15
+ - [CLI Usage](#cli-usage)
16
+ - [Programmatic API](#programmatic-api)
17
+ - [Key Mapping](#key-mapping)
18
+ - [Folder Mode](#folder-mode)
19
+ - [Configuration Reference](#configuration-reference)
20
+
21
+ ---
22
+
23
+ ## How it works
24
+
25
+ ```
26
+ Google Spreadsheet
27
+ ┌─────────────────────────────────────────────┐
28
+ │ Tab: "actions" │
29
+ │ key │ EN │ FR │
30
+ │ save │ Save │ Enregistrer │
31
+ │ cancel │ Cancel │ Annuler │
32
+ │ │ │
33
+ │ Tab: "errors" │ │
34
+ │ key │ EN │ FR │
35
+ │ not_found │ Not found│ Introuvable │
36
+ └─────────────────────────────────────────────┘
37
+
38
+ ▼ gsheets-i18n
39
+
40
+ en.json fr.json
41
+ { {
42
+ "actions": { "actions": {
43
+ "save": "Save", "save": "Enregistrer",
44
+ "cancel": "Cancel" "cancel": "Annuler"
45
+ }, },
46
+ "errors": { "errors": {
47
+ "not_found": "Not found" "not_found": "Introuvable"
48
+ } }
49
+ } }
50
+ ```
51
+
52
+ In your code you then access translations with dotted paths:
53
+
54
+ ```ts
55
+ t("actions.save") // → "Save"
56
+ t("errors.not_found") // → "Not found"
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Installation
62
+
63
+ ```bash
64
+ # As a dev dependency (recommended for most projects)
65
+ npm install --save-dev gsheets-i18n
66
+
67
+ # Or globally for CLI use
68
+ npm install -g gsheets-i18n
69
+ ```
70
+
71
+ **Requirements:** Node.js ≥ 18
72
+
73
+ ---
74
+
75
+ ## Google Setup
76
+
77
+ ### 1. Create a Service Account
78
+
79
+ 1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
80
+ 2. Create or select a project
81
+ 3. Go to **APIs & Services → Credentials**
82
+ 4. Click **Create Credentials → Service Account**
83
+ 5. Give it a name and click **Done**
84
+ 6. Open the service account, go to **Keys → Add Key → Create new key → JSON**
85
+ 7. Download the JSON file — this is your `service-account.json`
86
+
87
+ ### 2. Enable the required APIs
88
+
89
+ In **APIs & Services → Enabled APIs**, enable:
90
+
91
+ - **Google Sheets API**
92
+ - **Google Drive API** ← only required for [folder mode](#folder-mode)
93
+
94
+ ### 3. Share your spreadsheet with the service account
95
+
96
+ Open your Google Spreadsheet, click **Share**, and add the service account email (looks like `name@project.iam.gserviceaccount.com`) with **Viewer** permissions.
97
+
98
+ > ⚠️ Keep `service-account.json` out of version control. Add it to `.gitignore`.
99
+
100
+ ---
101
+
102
+ ## Spreadsheet Format
103
+
104
+ ### Basic structure
105
+
106
+ Each **tab** represents a namespace. Rows are translation keys, columns are languages.
107
+
108
+ ```
109
+ ┌──────────────────┬──────────────┬────────────────┬────────────────┐
110
+ │ key │ EN │ FR │ DE │
111
+ ├──────────────────┼──────────────┼────────────────┼────────────────┤
112
+ │ save │ Save │ Enregistrer │ Speichern │
113
+ │ cancel │ Cancel │ Annuler │ Abbrechen │
114
+ │ confirm │ Confirm │ Confirmer │ Bestätigen │
115
+ └──────────────────┴──────────────┴────────────────┴────────────────┘
116
+ ```
117
+
118
+ - **Row 1** — Header row: first cell is ignored (label for the key column), remaining cells are language identifiers.
119
+ - **Column A** — Translation key. Supports **dot notation** for nesting: `modal.title` → `{ modal: { title: "…" } }`.
120
+ - **Other columns** — One language per column. The header value is mapped to a locale code (see [Key Mapping](#key-mapping)).
121
+
122
+ ### Skipping tabs and columns
123
+
124
+ Prefix a tab name or column header with `_` to exclude it from output:
125
+
126
+ ```
127
+ Tab name "_notes" → ignored entirely
128
+ Column header "_dev" → ignored entirely
129
+ ```
130
+
131
+ ### Dot notation in keys
132
+
133
+ Use dots in your key to create nested output objects:
134
+
135
+ ```
136
+ key │ EN
137
+ modal.title │ Confirm action
138
+ modal.body │ Are you sure?
139
+ modal.actions.confirm │ Yes, proceed
140
+ modal.actions.cancel │ Go back
141
+ ```
142
+
143
+ Produces:
144
+
145
+ ```json
146
+ {
147
+ "modal": {
148
+ "title": "Confirm action",
149
+ "body": "Are you sure?",
150
+ "actions": {
151
+ "confirm": "Yes, proceed",
152
+ "cancel": "Go back"
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ ### Multiple tabs
159
+
160
+ Each tab becomes a top-level namespace in the output:
161
+
162
+ ```
163
+ Tab "actions" + key "save" → { "actions": { "save": "…" } }
164
+ Tab "errors" + key "404" → { "errors": { "404": "…" } }
165
+ ```
166
+
167
+ ---
168
+
169
+ ## CLI Usage
170
+
171
+ ### Sheet mode
172
+
173
+ Extract from a single spreadsheet (all non-internal tabs):
174
+
175
+ ```bash
176
+ gsheets-i18n sheet \
177
+ --sheet-id 4BxiM*****pms \
178
+ --key ./service-account.json \
179
+ --out ./src/locales
180
+ ```
181
+
182
+ Extract only one specific tab (by its numeric tab ID):
183
+
184
+ ```bash
185
+ gsheets-i18n sheet \
186
+ --sheet-id 4BxiM*****pms \
187
+ --key ./service-account.json \
188
+ --tab-id 0 \
189
+ --out ./src/locales
190
+ ```
191
+
192
+ > **Finding the tab ID:** Right-click a tab in your browser → "Copy link" — the URL contains `#gid=<tabId>`.
193
+
194
+ ### Folder mode
195
+
196
+ Scan a Drive folder recursively and extract from every spreadsheet found:
197
+
198
+ ```bash
199
+ gsheets-i18n folder \
200
+ --folder-id 1A2B3C4D5E6F7G8H9I0J \
201
+ --key ./service-account.json \
202
+ --out ./src/locales
203
+ ```
204
+
205
+ ### Output example
206
+
207
+ ```
208
+ ✔ Done in 1.24s
209
+
210
+ 📂 /your/project/src/locales
211
+
212
+ de.json (3 top-level keys)
213
+ en.json (3 top-level keys)
214
+ fr.json (3 top-level keys)
215
+ ```
216
+
217
+ ### Adding to package.json scripts
218
+
219
+ ```json
220
+ {
221
+ "scripts": {
222
+ "i18n:pull": "gsheets-i18n sheet --sheet-id YOUR_ID --key ./service-account.json --out ./src/locales"
223
+ }
224
+ }
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Programmatic API
230
+
231
+ ```ts
232
+ import { extract } from "gsheets-i18n";
233
+
234
+ const result = await extract({
235
+ serviceAccountKey: "./service-account.json",
236
+ source: {
237
+ mode: "sheet",
238
+ spreadsheetId: "4BxiM*****pms",
239
+ },
240
+ outputDir: "./src/locales",
241
+ });
242
+
243
+ console.log(`Generated ${result.files.length} files in ${result.durationMs}ms`);
244
+ // → Generated 3 files in 1240ms
245
+
246
+ for (const file of result.files) {
247
+ console.log(`${file.locale}: ${file.path} (${file.keyCount} keys)`);
248
+ }
249
+ ```
250
+
251
+ The `extract()` function returns a `Promise<ExtractResult>`:
252
+
253
+ ```ts
254
+ interface ExtractResult {
255
+ files: OutputFile[]; // One entry per generated file
256
+ durationMs: number; // Total wall-clock time
257
+ }
258
+
259
+ interface OutputFile {
260
+ locale: string; // e.g. "en", "fr", "zh-TW"
261
+ path: string; // Absolute path to the written file
262
+ keyCount: number; // Number of top-level namespace keys
263
+ }
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Key Mapping
269
+
270
+ Column headers in your sheet are mapped to BCP-47 locale codes. A wide range of spellings is supported out of the box:
271
+
272
+ | Header in sheet | Output locale code |
273
+ |----------------------------|--------------------|
274
+ | `EN`, `ENG`, `English` | `en` |
275
+ | `FR`, `FRE`, `French`, `Français` | `fr` |
276
+ | `DE`, `GER`, `German`, `Deutsch` | `de` |
277
+ | `ZH-TW`, `CHT`, `繁體中文` | `zh-TW` |
278
+ | `ZH-CN`, `CHS`, `简体中文` | `zh-CN` |
279
+ | `JA`, `JPN`, `Japanese`, `日本語` | `ja` |
280
+ | … and many more | |
281
+
282
+ ### Custom key map via the spreadsheet
283
+
284
+ Add a tab named `_keymap` to your spreadsheet to define project-specific mappings:
285
+
286
+ ```
287
+ ┌──────────────┬───────┬──────┬───────┐
288
+ │ (ignored) │ fr │ en │ pt-BR │
289
+ ├──────────────┼───────┼──────┼───────┤
290
+ │ Français │ fr │ │ │
291
+ │ Anglais │ │ en │ │
292
+ │ Brésilien │ │ │ pt-BR │
293
+ └──────────────┴───────┴──────┴───────┘
294
+ ```
295
+
296
+ ### Custom key map via CLI or API
297
+
298
+ Pass a JSON file to the CLI:
299
+
300
+ ```bash
301
+ gsheets-i18n sheet --sheet-id … --key-map ./my-key-map.json
302
+ ```
303
+
304
+ Or pass an object to the API:
305
+
306
+ ```ts
307
+ await extract({
308
+ // …
309
+ localeKeyMap: {
310
+ "Français": "fr",
311
+ "Anglais": "en",
312
+ "Brésilien": "pt-BR",
313
+ },
314
+ });
315
+ ```
316
+
317
+ **Priority (highest → lowest):**
318
+ 1. `_keymap` tab in the spreadsheet
319
+ 2. `localeKeyMap` option / `--key-map` file
320
+ 3. Built-in defaults
321
+
322
+ ---
323
+
324
+ ## Folder Mode
325
+
326
+ In folder mode, the library recursively scans a Google Drive folder:
327
+
328
+ - Each **sub-folder** name becomes a path component of the namespace
329
+ - Each **spreadsheet** name becomes the innermost namespace component
330
+ - Files and folders starting with `_` are **skipped**
331
+ - A spreadsheet named `_keymap` in any folder applies its mappings to everything inside that folder
332
+
333
+ **Example Drive structure:**
334
+
335
+ ```
336
+ 📁 my-translations/
337
+ ├── 📁 app/
338
+ │ ├── 📄 actions ← spreadsheet
339
+ │ └── 📄 errors ← spreadsheet
340
+ └── 📄 common ← spreadsheet
341
+ ```
342
+
343
+ Produces keys structured as:
344
+
345
+ ```json
346
+ {
347
+ "app": {
348
+ "actions": { "save": "…" },
349
+ "errors": { "404": "…" }
350
+ },
351
+ "common": { "yes": "…" }
352
+ }
353
+ ```
354
+
355
+ ---
356
+
357
+ ## Configuration Reference
358
+
359
+ ### `extract(options)` — full options
360
+
361
+ | Option | Type | Default | Description |
362
+ |---------------------|-----------------------------|--------------------------|-------------|
363
+ | `serviceAccountKey` | `string \| ServiceAccountKey` | *(required)* | Path to key file or parsed key object |
364
+ | `source` | `SheetModeOptions \| FolderModeOptions` | *(required)* | What to extract |
365
+ | `outputDir` | `string` | `"./i18n"` | Directory for output files |
366
+ | `localeKeyMap` | `Record<string, string>` | `{}` | Custom header → locale mappings |
367
+ | `includeEmpty` | `boolean` | `false` | Write keys with empty values |
368
+ | `indent` | `number \| string` | `2` | JSON indentation (`2`, `4`, `"\t"`) |
369
+
370
+ ### `SheetModeOptions`
371
+
372
+ | Option | Type | Default | Description |
373
+ |-----------------|----------|---------|-------------|
374
+ | `mode` | `"sheet"` | *(required)* | |
375
+ | `spreadsheetId` | `string` | *(required)* | Google Spreadsheet ID |
376
+ | `tabId` | `number` | all tabs | Specific tab to extract |
377
+ | `startColumn` | `number` | `0` | Skip leading columns |
378
+
379
+ ### `FolderModeOptions`
380
+
381
+ | Option | Type | Default | Description |
382
+ |------------|-----------|---------|-------------|
383
+ | `mode` | `"folder"` | *(required)* | |
384
+ | `folderId` | `string` | *(required)* | Google Drive folder ID |
385
+
386
+ ### CLI flags — `sheet` command
387
+
388
+ | Flag | Default | Description |
389
+ |---------------------------|--------------------------|-------------|
390
+ | `-s, --sheet-id <id>` | *(required)* | Spreadsheet ID |
391
+ | `-k, --key <path>` | `./service-account.json` | Key file path |
392
+ | `-o, --out <path>` | `./i18n` | Output directory |
393
+ | `-t, --tab-id <id>` | all tabs | Numeric tab ID |
394
+ | `--start-column <n>` | `0` | Skip leading columns |
395
+ | `--include-empty` | `false` | Include empty values |
396
+ | `--indent <n\|"tab">` | `2` | JSON indentation |
397
+ | `--key-map <path>` | — | Custom key map JSON |
398
+
399
+ ### CLI flags — `folder` command
400
+
401
+ | Flag | Default | Description |
402
+ |-----------------------|--------------------------|-------------|
403
+ | `-f, --folder-id <id>` | *(required)* | Drive folder ID |
404
+ | `-k, --key <path>` | `./service-account.json` | Key file path |
405
+ | `-o, --out <path>` | `./i18n` | Output directory |
406
+ | `--include-empty` | `false` | Include empty values |
407
+ | `--indent <n\|"tab">` | `2` | JSON indentation |
408
+ | `--key-map <path>` | — | Custom key map JSON |
409
+
410
+ ---
411
+
412
+ ## License
413
+
414
+ MIT
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const index_js_1 = require("../index.js");
6
+ const promises_1 = require("node:fs/promises");
7
+ const node_path_1 = require("node:path");
8
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
9
+ function parseIndent(value) {
10
+ if (value === "tab" || value === "\t")
11
+ return "\t";
12
+ const num = Number(value);
13
+ if (!Number.isNaN(num) && num >= 0 && num <= 8)
14
+ return num;
15
+ throw new Error(`Invalid indent value: "${value}". Use a number 0–8 or "tab".`);
16
+ }
17
+ function fatal(message) {
18
+ console.error(`\n ✖ ${message}\n`);
19
+ process.exit(1);
20
+ }
21
+ // ─── CLI definition ───────────────────────────────────────────────────────────
22
+ const program = new commander_1.Command();
23
+ program
24
+ .name("sheets-i18n")
25
+ .description("Generate i18n JSON files from a Google Spreadsheet or Drive folder.")
26
+ .version("1.0.0");
27
+ // ── sheet command ─────────────────────────────────────────────────────────────
28
+ program
29
+ .command("sheet")
30
+ .description("Extract from a single Google Spreadsheet")
31
+ .requiredOption("-s, --sheet-id <spreadsheetId>", "Google Spreadsheet ID (from the URL)")
32
+ .requiredOption("-k, --key <path>", "Path to the Service Account JSON key file", "./service-account.json")
33
+ .option("-o, --out <path>", "Output directory for the generated JSON files", "./i18n")
34
+ .option("-t, --tab-id <id>", "Numeric tab ID to extract (default: all tabs)")
35
+ .option("--start-column <n>", "Zero-based column index to start reading from", "0")
36
+ .option("--include-empty", "Include keys with empty values in the output", false)
37
+ .option("--indent <value>", 'JSON indentation: a number of spaces or "tab"', "2")
38
+ .option("--key-map <path>", "Path to a JSON file with custom locale key mappings")
39
+ .action(async (opts) => {
40
+ let localeKeyMap;
41
+ if (opts.keyMap) {
42
+ try {
43
+ const raw = await (0, promises_1.readFile)((0, node_path_1.resolve)(process.cwd(), opts.keyMap), "utf-8");
44
+ localeKeyMap = JSON.parse(raw);
45
+ }
46
+ catch {
47
+ fatal(`Could not read key map file: ${opts.keyMap}`);
48
+ return;
49
+ }
50
+ }
51
+ console.log("\n ⏳ Extracting translations…\n");
52
+ try {
53
+ const result = await (0, index_js_1.extract)({
54
+ serviceAccountKey: opts.key,
55
+ source: {
56
+ mode: "sheet",
57
+ spreadsheetId: opts.sheetId,
58
+ tabId: opts.tabId !== undefined ? Number(opts.tabId) : undefined,
59
+ startColumn: Number(opts.startColumn),
60
+ },
61
+ outputDir: opts.out,
62
+ localeKeyMap,
63
+ includeEmpty: opts.includeEmpty,
64
+ indent: parseIndent(opts.indent),
65
+ });
66
+ console.log(` ✔ Done in ${(result.durationMs / 1000).toFixed(2)}s\n`);
67
+ console.log(` 📂 ${(0, node_path_1.resolve)(process.cwd(), opts.out)}\n`);
68
+ for (const file of result.files) {
69
+ console.log(` ${file.locale}.json (${file.keyCount} top-level keys)`);
70
+ }
71
+ console.log();
72
+ }
73
+ catch (err) {
74
+ fatal(err instanceof Error ? err.message : String(err));
75
+ }
76
+ });
77
+ // ── folder command ────────────────────────────────────────────────────────────
78
+ program
79
+ .command("folder")
80
+ .description("Extract from all spreadsheets inside a Google Drive folder")
81
+ .requiredOption("-f, --folder-id <folderId>", "Google Drive folder ID (from the URL)")
82
+ .requiredOption("-k, --key <path>", "Path to the Service Account JSON key file", "./service-account.json")
83
+ .option("-o, --out <path>", "Output directory for the generated JSON files", "./i18n")
84
+ .option("--include-empty", "Include keys with empty values in the output", false)
85
+ .option("--indent <value>", 'JSON indentation: a number of spaces or "tab"', "2")
86
+ .option("--key-map <path>", "Path to a JSON file with custom locale key mappings")
87
+ .action(async (opts) => {
88
+ let localeKeyMap;
89
+ if (opts.keyMap) {
90
+ try {
91
+ const raw = await (0, promises_1.readFile)((0, node_path_1.resolve)(process.cwd(), opts.keyMap), "utf-8");
92
+ localeKeyMap = JSON.parse(raw);
93
+ }
94
+ catch {
95
+ fatal(`Could not read key map file: ${opts.keyMap}`);
96
+ return;
97
+ }
98
+ }
99
+ console.log("\n ⏳ Extracting translations…\n");
100
+ try {
101
+ const result = await (0, index_js_1.extract)({
102
+ serviceAccountKey: opts.key,
103
+ source: {
104
+ mode: "folder",
105
+ folderId: opts.folderId,
106
+ },
107
+ outputDir: opts.out,
108
+ localeKeyMap,
109
+ includeEmpty: opts.includeEmpty,
110
+ indent: parseIndent(opts.indent),
111
+ });
112
+ console.log(` ✔ Done in ${(result.durationMs / 1000).toFixed(2)}s\n`);
113
+ console.log(` 📂 ${(0, node_path_1.resolve)(process.cwd(), opts.out)}\n`);
114
+ for (const file of result.files) {
115
+ console.log(` ${file.locale}.json (${file.keyCount} top-level keys)`);
116
+ }
117
+ console.log();
118
+ }
119
+ catch (err) {
120
+ fatal(err instanceof Error ? err.message : String(err));
121
+ }
122
+ });
123
+ // ─── Parse ────────────────────────────────────────────────────────────────────
124
+ program.parse();
125
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extract = extract;
4
+ const auth_js_1 = require("./lib/auth.js");
5
+ const sheetExtractor_js_1 = require("./lib/sheetExtractor.js");
6
+ const folderExtractor_js_1 = require("./lib/folderExtractor.js");
7
+ const writer_js_1 = require("./lib/writer.js");
8
+ /**
9
+ * Extracts translations from Google Sheets and writes one JSON file per locale.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { extract } from "sheets-i18n";
14
+ *
15
+ * await extract({
16
+ * serviceAccountKey: "./service-account.json",
17
+ * source: {
18
+ * mode: "sheet",
19
+ * spreadsheetId: "4BxiM********upms",
20
+ * },
21
+ * outputDir: "./src/locales",
22
+ * });
23
+ * ```
24
+ */
25
+ async function extract(options) {
26
+ const startTime = Date.now();
27
+ const { serviceAccountKey, source, outputDir = "./i18n", localeKeyMap, includeEmpty = false, indent = 2, } = options;
28
+ // 1. Authenticate
29
+ const auth = await (0, auth_js_1.createAuthClient)(serviceAccountKey);
30
+ // 2. Extract translations
31
+ let translations;
32
+ if (source.mode === "sheet") {
33
+ translations = await (0, sheetExtractor_js_1.extractFromSheet)({
34
+ auth,
35
+ spreadsheetId: source.spreadsheetId,
36
+ tabId: source.tabId,
37
+ startColumn: source.startColumn,
38
+ includeEmpty,
39
+ userKeyMap: localeKeyMap,
40
+ });
41
+ }
42
+ else {
43
+ translations = await (0, folderExtractor_js_1.extractFromFolder)({
44
+ auth,
45
+ folderId: source.folderId,
46
+ includeEmpty,
47
+ userKeyMap: localeKeyMap,
48
+ });
49
+ }
50
+ // 3. Write files
51
+ const files = await (0, writer_js_1.writeTranslationFiles)(translations, outputDir, indent);
52
+ return {
53
+ files,
54
+ durationMs: Date.now() - startTime,
55
+ };
56
+ }
57
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;AAoCA,0BA2CC;AA/ED,2CAAiD;AACjD,+DAA2D;AAC3D,iEAA6D;AAC7D,+CAAwD;AAgBxD;;;;;;;;;;;;;;;;GAgBG;AACI,KAAK,UAAU,OAAO,CAAC,OAAuB;IACnD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAE7B,MAAM,EACJ,iBAAiB,EACjB,MAAM,EACN,SAAS,GAAG,QAAQ,EACpB,YAAY,EACZ,YAAY,GAAG,KAAK,EACpB,MAAM,GAAG,CAAC,GACX,GAAG,OAAO,CAAC;IAEZ,kBAAkB;IAClB,MAAM,IAAI,GAAG,MAAM,IAAA,0BAAgB,EAAC,iBAAiB,CAAC,CAAC;IAEvD,0BAA0B;IAC1B,IAAI,YAAY,CAAC;IAEjB,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC5B,YAAY,GAAG,MAAM,IAAA,oCAAgB,EAAC;YACpC,IAAI;YACJ,aAAa,EAAE,MAAM,CAAC,aAAa;YACnC,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,YAAY;YACZ,UAAU,EAAE,YAAY;SACzB,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,YAAY,GAAG,MAAM,IAAA,sCAAiB,EAAC;YACrC,IAAI;YACJ,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,YAAY;YACZ,UAAU,EAAE,YAAY;SACzB,CAAC,CAAC;IACL,CAAC;IAED,iBAAiB;IACjB,MAAM,KAAK,GAAG,MAAM,IAAA,iCAAqB,EAAC,YAAY,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IAE3E,OAAO;QACL,KAAK;QACL,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;KACnC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAuthClient = createAuthClient;
4
+ const googleapis_1 = require("googleapis");
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_path_1 = require("node:path");
7
+ const SCOPES = [
8
+ "https://www.googleapis.com/auth/spreadsheets.readonly",
9
+ "https://www.googleapis.com/auth/drive.readonly",
10
+ ];
11
+ /**
12
+ * Builds an authenticated Google JWT client from a Service Account key.
13
+ *
14
+ * @param keySource - Either a file-system path to the key JSON, or the parsed object.
15
+ */
16
+ async function createAuthClient(keySource) {
17
+ let key;
18
+ if (typeof keySource === "string") {
19
+ const absolutePath = (0, node_path_1.resolve)(process.cwd(), keySource);
20
+ const raw = await (0, promises_1.readFile)(absolutePath, "utf-8");
21
+ key = JSON.parse(raw);
22
+ }
23
+ else {
24
+ key = keySource;
25
+ }
26
+ const client = new googleapis_1.google.auth.JWT({
27
+ email: key.client_email,
28
+ key: key.private_key,
29
+ scopes: SCOPES,
30
+ });
31
+ await client.authorize();
32
+ return client;
33
+ }
34
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/lib/auth.ts"],"names":[],"mappings":";;AAeA,4CAqBC;AApCD,2CAAoC;AACpC,+CAA4C;AAC5C,yCAAoC;AAGpC,MAAM,MAAM,GAAG;IACb,uDAAuD;IACvD,gDAAgD;CACjD,CAAC;AAEF;;;;GAIG;AACI,KAAK,UAAU,gBAAgB,CACpC,SAAqC;IAErC,IAAI,GAAsB,CAAC;IAE3B,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;QAClC,MAAM,YAAY,GAAG,IAAA,mBAAO,EAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,CAAC,CAAC;QACvD,MAAM,GAAG,GAAG,MAAM,IAAA,mBAAQ,EAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAClD,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAsB,CAAC;IAC7C,CAAC;SAAM,CAAC;QACN,GAAG,GAAG,SAAS,CAAC;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,mBAAM,CAAC,IAAI,CAAC,GAAG,CAAC;QACjC,KAAK,EAAE,GAAG,CAAC,YAAY;QACvB,GAAG,EAAE,GAAG,CAAC,WAAW;QACpB,MAAM,EAAE,MAAM;KACf,CAAC,CAAC;IAEH,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;IACzB,OAAO,MAAM,CAAC;AAChB,CAAC"}