From 2dcca1817eec4a53101db130409c87c084569b22 Mon Sep 17 00:00:00 2001 From: Maxime Guilbault Date: Wed, 10 Jun 2026 16:42:10 +0200 Subject: [PATCH 1/2] Feat: add multi lines and set lines --- Readme.md | 49 +++++++++++- package-lock.json | 6 +- src/config/WordingConfig.ts | 35 ++++++--- src/google/Drive.ts | 148 ++++++++++++++++++++++++++++++++---- src/index.ts | 65 +++++++++++----- src/utils.ts | 18 +++++ 6 files changed, 268 insertions(+), 53 deletions(-) diff --git a/Readme.md b/Readme.md index 233f4a2..5256e1b 100644 --- a/Readme.md +++ b/Readme.md @@ -36,7 +36,9 @@ You can find a sample sheet [here](https://docs.google.com/spreadsheets/d/18Zf_X ```json { "scripts": { - "upgrade-wording": "sync-wording --upgrade" + "upgrade-wording": "sync-wording --upgrade", + "add-wording": "sync-wording --add", + "set-wording":"sync-wording --set" } } ``` @@ -93,12 +95,55 @@ Now the tool will warn you when you update wording containing invalid translatio ## Options -This tools support 3 options +This tools support the following options - **`--config`** : Configuration path - **`--upgrade`** : Export sheet in local xlsx file that you can commit for later edit. It prevent risks to have unwanted wording changes when you fix bugs. And then update wording - **`--update`** : Update wording files from local xlsx file - **`--invalid`** : (error|warning) exist with error when invalid translations found or just warn +- **`--add`** : Add one or more wording lines to the remote Google Sheet. Each line is a key followed by one translation per configured language. To add multiple lines in a single command, repeat the pattern (key + translations). +- **`--set`** : Update one or more existing wording lines in the remote Google Sheet by key. Same argument format as `--add`. The key must already exist in the sheet. + +### Add wording lines + +Use `--add` to push new keys and translations directly to the Google Sheet (first sheet in `sheetNames`). + +Single line (2 languages: en, fr): + +```bash +sync-wording --add user.email_title "Email" "E-mail" +``` + +Multiple lines at once — repeat `key + translations` for each language: + +```bash +sync-wording --add \ + user.email_title "Email" "E-mail" \ + user.phone_title "Phone" "Téléphone" \ + user.address_title "Address" "Adresse" +``` + +The number of values after each key must match the number of languages defined in `wording_config.json`. With 2 languages, each line requires 3 arguments (1 key + 2 translations). + +### Update wording lines + +Use `--set` to update translations for existing keys. The tool searches for each key in the configured sheets (`sheetNames`) and updates the corresponding row. + +Single line: + +```bash +sync-wording --set user.email_title "Email address" "Adresse e-mail" +``` + +Multiple lines at once: + +```bash +sync-wording --set \ + user.email_title "Email address" "Adresse e-mail" \ + user.phone_title "Phone number" "Numéro de téléphone" +``` + +If a key is not found, the command fails with an error. If the same key exists in multiple sheets, the command also fails to avoid ambiguous updates. ## Complete Configuration diff --git a/package-lock.json b/package-lock.json index a9f4441..a24932e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -120,7 +120,6 @@ "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -1381,7 +1380,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1544,7 +1542,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.0.tgz", "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "dev": true, - "peer": true, "requires": { "undici-types": ">=7.24.0 <7.24.7" } @@ -2312,8 +2309,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "peer": true + "dev": true }, "undefsafe": { "version": "2.0.5", diff --git a/src/config/WordingConfig.ts b/src/config/WordingConfig.ts index 760aa3e..fe230c9 100644 --- a/src/config/WordingConfig.ts +++ b/src/config/WordingConfig.ts @@ -1,14 +1,19 @@ export interface Validation { - column : string; + column: string; expected: string; } export class LanguageConfig { name: string; column: string; output: string; - validation : Validation | null - - constructor(name: string, config: any, defaultOutputDir: string, validation : Validation | null) { + validation: Validation | null; + + constructor( + name: string, + config: any, + defaultOutputDir: string, + validation: Validation | null, + ) { this.name = name; this.column = config.column; if (config.output) { @@ -39,13 +44,13 @@ export class WordingConfig { languages: LanguageConfig[]; format: string; ignoreEmptyKeys: boolean; - validation : Validation | null; + validation: Validation | null; constructor(jsonConfig: any) { this.wording_file = this.getOrDefault( jsonConfig, "wording_file", - "./wording.xlsx" + "./wording.xlsx", ); this.credentials = this.getOrDefault(jsonConfig, "credentials", ""); @@ -60,26 +65,34 @@ export class WordingConfig { this.output_dir = this.getOrDefault( jsonConfig, "output_dir", - "./src/assets/strings/" + "./src/assets/strings/", ); this.languages = []; this.format = this.getOrDefault(jsonConfig, "format", "json"); - this.ignoreEmptyKeys = this.getOrDefault(jsonConfig, "ignoreEmptyKeys", false); + this.ignoreEmptyKeys = this.getOrDefault( + jsonConfig, + "ignoreEmptyKeys", + false, + ); - this.validation = this.getOrDefault(jsonConfig, "validation", null) + this.validation = this.getOrDefault(jsonConfig, "validation", null); for (const language in jsonConfig.languages) { if (jsonConfig.languages.hasOwnProperty(language)) { const element = jsonConfig.languages[language]; this.languages.push( - new LanguageConfig(language, element, this.output_dir, this.validation) + new LanguageConfig( + language, + element, + this.output_dir, + this.validation, + ), ); } } - } private getOrDefault(source: any, key: string, defaultValue: T): T { diff --git a/src/google/Drive.ts b/src/google/Drive.ts index 0371220..b073570 100644 --- a/src/google/Drive.ts +++ b/src/google/Drive.ts @@ -1,8 +1,13 @@ import fs from "fs"; -import path from "path"; import { google, drive_v3 } from "googleapis"; import { OAuth2Client } from "google-auth-library"; -import { resolve } from "dns"; +import { LanguageConfig } from "../config/WordingConfig"; + +interface KeyLocation { + sheetName: string; + rowIndex: number; +} + export class Drive { drive: drive_v3.Drive; @@ -32,23 +37,138 @@ export class Drive { ); } - async addLine(spreadsheetId: string, sheetName: string, key: string, ...values: string[]) { + async addLines( + spreadsheetId: string, + sheetName: string, + lines: { key: string; values: string[] }[] + ) { const service = google.sheets({ version: "v4", auth: this.auth }); - try { - const result = await service.spreadsheets.values.append({ - spreadsheetId, - range: sheetName, + const result = await service.spreadsheets.values.append({ + spreadsheetId, + range: sheetName, + valueInputOption: "RAW", + requestBody: { + values: lines.map(({ key, values }) => [key, ...values]), + }, + }); + + console.log( + `${lines.length} line(s) appended (${result.data.updates?.updatedCells} cells).` + ); + return result; + } + + async updateLines( + spreadsheetId: string, + sheetNames: string[] | undefined, + keyColumn: string, + startRow: number, + languages: LanguageConfig[], + lines: { key: string; values: string[] }[] + ) { + const service = google.sheets({ version: "v4", auth: this.auth }); + const resolvedSheetNames = await this.resolveSheetNames( + service, + spreadsheetId, + sheetNames + ); + const keyLocations = await this.findKeyLocations( + service, + spreadsheetId, + resolvedSheetNames, + keyColumn, + startRow + ); + + const updates: { range: string; values: string[][] }[] = []; + for (const line of lines) { + const location = keyLocations.get(line.key); + if (!location) { + throw new Error(`Key not found: "${line.key}"`); + } + + languages.forEach((language, index) => { + updates.push({ + range: `${location.sheetName}!${language.column}${location.rowIndex}`, + values: [[line.values[index]]], + }); + }); + } + + const result = await service.spreadsheets.values.batchUpdate({ + spreadsheetId, + requestBody: { valueInputOption: "RAW", - requestBody: { - values: [[key, ...values]], - }, + data: updates, + }, + }); + + console.log( + `${lines.length} line(s) updated (${result.data.totalUpdatedCells} cells).` + ); + return result; + } + + private async resolveSheetNames( + service: ReturnType, + spreadsheetId: string, + sheetNames: string[] | undefined + ): Promise { + if (sheetNames && sheetNames.length > 0) { + return sheetNames; + } + + const meta = await service.spreadsheets.get({ spreadsheetId }); + const titles = meta.data.sheets + ?.map((sheet) => sheet.properties?.title) + .filter((title): title is string => Boolean(title)); + + if (!titles?.length) { + throw new Error("No sheets found in spreadsheet"); + } + + return titles; + } + + private async findKeyLocations( + service: ReturnType, + spreadsheetId: string, + sheetNames: string[], + keyColumn: string, + startRow: number + ): Promise> { + const keyLocations = new Map(); + + for (const sheetName of sheetNames) { + const response = await service.spreadsheets.values.get({ + spreadsheetId, + range: `${sheetName}!${keyColumn}${startRow}:${keyColumn}`, }); - console.log(`${result.data.updates?.updatedCells} cells appended.`); - return result; - } catch (err) { - throw err; + const keys = response.data.values ?? []; + keys.forEach((row, index) => { + const key = row[0]; + if (!key) { + return; + } + + const location: KeyLocation = { + sheetName, + rowIndex: startRow + index, + }; + + const existing = keyLocations.get(key); + if (existing) { + throw new Error( + `Duplicate key "${key}" found in sheets "${existing.sheetName}" and "${sheetName}"` + ); + } + + keyLocations.set(key, location); + }); } + + return keyLocations; } async exportAsXlsx(fileId: string, output: string, mimeType: string) { diff --git a/src/index.ts b/src/index.ts index 348c4ed..9aaa461 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import colors from 'colors/safe'; +import colors from "colors/safe"; import { program } from "commander"; import { loadConfiguration } from "./config/WordingConfigLoader"; import { AngularJsonWordingExporter } from "./exporters/AngularJsonWordingExporter"; @@ -9,17 +9,29 @@ import { NestedJsonWordingExporter } from "./exporters/NestedJsonWordingExporter import { WordingExporter } from "./exporters/WordingExporter"; import { Drive } from "./google/Drive"; import { GoogleAuth } from "./google/GoogleAuth"; +import { parseWordingLines } from "./utils"; import { WordingLoader, WordingResult } from "./WordingLoader"; console.log("Running sync wording"); program .description("Upgrade application wording from Google Sheet") - .option("--add ", "add a wording line to remote Google Sheet") + .option( + "--add ", + "add wording line(s) to remote Google Sheet (key followed by one value per language; repeat the pattern for multiple lines)", + ) + .option( + "--set ", + "update wording line(s) in remote Google Sheet by key (key followed by one value per language; repeat the pattern for multiple lines)", + ) .option("--config ", "wording config path", "wording_config.json") .option("--upgrade", "upgrade wording from remote Google Sheet") .option("--update", "update wording from local xlsx") - .option("--invalid ", "error|warning on invalid translation", "warning") + .option( + "--invalid ", + "error|warning on invalid translation", + "warning", + ) .option("-v, --verbose", "show verbose logs") .parse(process.argv); @@ -27,23 +39,23 @@ if (!process.argv.slice(2).length) { program.outputHelp(); } -function getExporter(format: String) : WordingExporter { +function getExporter(format: String): WordingExporter { if (format === "angular-json") { - return new AngularJsonWordingExporter() + return new AngularJsonWordingExporter(); } else if (format === "flat-json") { return new FlatJsonWordingExporter(); } return new NestedJsonWordingExporter(); } -function printReport(language: string, result : WordingResult) : void { +function printReport(language: string, result: WordingResult): void { if (result.hasInvalidKeys) { - console.warn(colors.yellow(`Invalid translations found for language : ${language}`)) - result.invalidKeys.forEach((k) => - console.warn(colors.yellow(`\t"${k}"`)) - ) + console.warn( + colors.yellow(`Invalid translations found for language : ${language}`), + ); + result.invalidKeys.forEach((k) => console.warn(colors.yellow(`\t"${k}"`))); } -}; +} const options = program.opts(); @@ -59,9 +71,22 @@ loadConfiguration(options.config).then(async (config) => { } if (options.add) { - const [key, ...values] = options.add; + const lines = parseWordingLines(options.add, config.languages.length); const auth = await new GoogleAuth().authorize(config.credentials, scopes); - await new Drive(auth).addLine(config.sheetId, config.sheetNames[0], key, ...values); + await new Drive(auth).addLines(config.sheetId, config.sheetNames[0], lines); + } + + if (options.set) { + const lines = parseWordingLines(options.set, config.languages.length); + const auth = await new GoogleAuth().authorize(config.credentials, scopes); + await new Drive(auth).updateLines( + config.sheetId, + config.sheetNames, + config.keyColumn, + config.sheetStartIndex, + config.languages, + lines, + ); } if (options.upgrade) { @@ -71,7 +96,7 @@ loadConfiguration(options.config).then(async (config) => { await new Drive(auth).exportAsXlsx( config.sheetId, config.wording_file, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ); } @@ -80,18 +105,16 @@ loadConfiguration(options.config).then(async (config) => { const loader = new WordingLoader(config); const exporter = getExporter(config.format); - let hasError = false - config.languages.forEach(language => { + let hasError = false; + config.languages.forEach((language) => { const result = loader.loadWording(language, config.ignoreEmptyKeys); exporter.export(language.name, result.wordings, language.output); - printReport(language.name, result) - hasError = hasError || result.hasInvalidKeys + printReport(language.name, result); + hasError = hasError || result.hasInvalidKeys; }); if (hasError && options.invalid === "error") { - process.exitCode = 1 + process.exitCode = 1; } } }); - - diff --git a/src/utils.ts b/src/utils.ts index bdc9d12..e601cfd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,3 +8,21 @@ function concat(first: Map, other: Map): Map { }); return result; } + +export function parseWordingLines( + args: string[], + languageCount: number +): { key: string; values: string[] }[] { + const lineSize = 1 + languageCount; + if (args.length % lineSize !== 0) { + throw new Error( + `Invalid --add arguments: expected groups of ${lineSize} (key + ${languageCount} translation(s)), got ${args.length} value(s)` + ); + } + + const lines: { key: string; values: string[] }[] = []; + for (let i = 0; i < args.length; i += lineSize) { + lines.push({ key: args[i], values: args.slice(i + 1, i + lineSize) }); + } + return lines; +} From 1e57278550c4badd96e3dcfa239ddc6cecf6c82b Mon Sep 17 00:00:00 2001 From: Maxime Guilbault Date: Wed, 10 Jun 2026 18:28:54 +0200 Subject: [PATCH 2/2] Fix: select sheet and insert good column --- Readme.md | 27 ++++--- src/google/Drive.ts | 169 +++++++++++++++++++++++++------------------- src/index.ts | 29 ++++++-- 3 files changed, 137 insertions(+), 88 deletions(-) diff --git a/Readme.md b/Readme.md index 5256e1b..ab6a268 100644 --- a/Readme.md +++ b/Readme.md @@ -101,33 +101,42 @@ This tools support the following options - **`--upgrade`** : Export sheet in local xlsx file that you can commit for later edit. It prevent risks to have unwanted wording changes when you fix bugs. And then update wording - **`--update`** : Update wording files from local xlsx file - **`--invalid`** : (error|warning) exist with error when invalid translations found or just warn -- **`--add`** : Add one or more wording lines to the remote Google Sheet. Each line is a key followed by one translation per configured language. To add multiple lines in a single command, repeat the pattern (key + translations). -- **`--set`** : Update one or more existing wording lines in the remote Google Sheet by key. Same argument format as `--add`. The key must already exist in the sheet. +- **`--add`** : Add one or more wording lines to the remote Google Sheet. Each line is a key followed by one translation per configured language (same order as `languages` in config). To add multiple lines in a single command, repeat the pattern (key + translations). +- **`--set`** : Update one or more existing wording lines in the remote Google Sheet by key. Same argument format as `--add`. The key must already exist in the target sheet. +- **`--sheet`** : Sheet tab name for `--add` / `--set` (default: first entry in `sheetNames`). ### Add wording lines -Use `--add` to push new keys and translations directly to the Google Sheet (first sheet in `sheetNames`). +Use `--add` to push new keys and translations directly to the Google Sheet. By default, the first sheet in `sheetNames` is used; pass `--sheet` to target a specific tab. -Single line (2 languages: en, fr): +Single line (2 languages: fr, en — order matches `languages` in config): ```bash -sync-wording --add user.email_title "Email" "E-mail" +sync-wording --add user.email_title "E-mail" "Email" +``` + +Target a specific sheet: + +```bash +sync-wording --sheet MyApp --add user.email_title "E-mail" "Email" ``` Multiple lines at once — repeat `key + translations` for each language: ```bash sync-wording --add \ - user.email_title "Email" "E-mail" \ - user.phone_title "Phone" "Téléphone" \ - user.address_title "Address" "Adresse" + user.email_title "E-mail" "Email" \ + user.phone_title "Téléphone" "Phone" \ + user.address_title "Adresse" "Address" ``` +Keys are written to `keyColumn` and each translation to its language `column` from the config (e.g. key in `A`, `en` in `C`, `fr` in `D`). + The number of values after each key must match the number of languages defined in `wording_config.json`. With 2 languages, each line requires 3 arguments (1 key + 2 translations). ### Update wording lines -Use `--set` to update translations for existing keys. The tool searches for each key in the configured sheets (`sheetNames`) and updates the corresponding row. +Use `--set` to update translations for existing keys in the sheet selected via `--sheet` or the first entry in `sheetNames`. Single line: diff --git a/src/google/Drive.ts b/src/google/Drive.ts index b073570..d848980 100644 --- a/src/google/Drive.ts +++ b/src/google/Drive.ts @@ -3,9 +3,10 @@ import { google, drive_v3 } from "googleapis"; import { OAuth2Client } from "google-auth-library"; import { LanguageConfig } from "../config/WordingConfig"; -interface KeyLocation { - sheetName: string; - rowIndex: number; +export interface SheetWriteConfig { + keyColumn: string; + languages: LanguageConfig[]; + sheetStartIndex: number; } export class Drive { @@ -40,15 +41,16 @@ export class Drive { async addLines( spreadsheetId: string, sheetName: string, - lines: { key: string; values: string[] }[] + lines: { key: string; values: string[] }[], + writeConfig: SheetWriteConfig ) { const service = google.sheets({ version: "v4", auth: this.auth }); const result = await service.spreadsheets.values.append({ spreadsheetId, - range: sheetName, + range: `${sheetName}!${writeConfig.keyColumn}:${writeConfig.keyColumn}`, valueInputOption: "RAW", requestBody: { - values: lines.map(({ key, values }) => [key, ...values]), + values: lines.map((line) => this.buildAppendRow(line, writeConfig)), }, }); @@ -60,39 +62,29 @@ export class Drive { async updateLines( spreadsheetId: string, - sheetNames: string[] | undefined, - keyColumn: string, - startRow: number, - languages: LanguageConfig[], - lines: { key: string; values: string[] }[] + sheetName: string, + lines: { key: string; values: string[] }[], + writeConfig: SheetWriteConfig ) { const service = google.sheets({ version: "v4", auth: this.auth }); - const resolvedSheetNames = await this.resolveSheetNames( - service, - spreadsheetId, - sheetNames - ); - const keyLocations = await this.findKeyLocations( + const keyRows = await this.findKeyRows( service, spreadsheetId, - resolvedSheetNames, - keyColumn, - startRow + sheetName, + writeConfig.keyColumn, + writeConfig.sheetStartIndex ); const updates: { range: string; values: string[][] }[] = []; for (const line of lines) { - const location = keyLocations.get(line.key); - if (!location) { + const rowIndex = keyRows.get(line.key); + if (!rowIndex) { throw new Error(`Key not found: "${line.key}"`); } - languages.forEach((language, index) => { - updates.push({ - range: `${location.sheetName}!${language.column}${location.rowIndex}`, - values: [[line.values[index]]], - }); - }); + updates.push( + ...this.buildTranslationUpdates(sheetName, rowIndex, line, writeConfig) + ); } const result = await service.spreadsheets.values.batchUpdate({ @@ -109,66 +101,87 @@ export class Drive { return result; } - private async resolveSheetNames( - service: ReturnType, - spreadsheetId: string, - sheetNames: string[] | undefined - ): Promise { - if (sheetNames && sheetNames.length > 0) { - return sheetNames; + private validateLine( + line: { key: string; values: string[] }, + writeConfig: SheetWriteConfig + ) { + if (line.values.length !== writeConfig.languages.length) { + throw new Error( + `Invalid values for key "${line.key}": expected ${writeConfig.languages.length} translation(s), got ${line.values.length}` + ); } + } - const meta = await service.spreadsheets.get({ spreadsheetId }); - const titles = meta.data.sheets - ?.map((sheet) => sheet.properties?.title) - .filter((title): title is string => Boolean(title)); + private buildAppendRow( + line: { key: string; values: string[] }, + writeConfig: SheetWriteConfig + ): string[] { + this.validateLine(line, writeConfig); - if (!titles?.length) { - throw new Error("No sheets found in spreadsheet"); - } + const keyColumnIndex = columnToIndex(writeConfig.keyColumn); + const languageColumnIndexes = writeConfig.languages.map((language) => + columnToIndex(language.column) + ); + const maxColumnIndex = Math.max(keyColumnIndex, ...languageColumnIndexes); + + const row = new Array(maxColumnIndex - keyColumnIndex + 1).fill(""); + row[0] = line.key; + writeConfig.languages.forEach((language, index) => { + const columnOffset = columnToIndex(language.column) - keyColumnIndex; + if (columnOffset < 0) { + throw new Error( + `Language column "${language.column}" must not be before keyColumn "${writeConfig.keyColumn}"` + ); + } - return titles; + row[columnOffset] = line.values[index]; + }); + + return row; } - private async findKeyLocations( + private buildTranslationUpdates( + sheetName: string, + rowIndex: number, + line: { key: string; values: string[] }, + writeConfig: SheetWriteConfig + ): { range: string; values: string[][] }[] { + this.validateLine(line, writeConfig); + + return writeConfig.languages.map((language, index) => ({ + range: `${sheetName}!${language.column}${rowIndex}`, + values: [[line.values[index]]], + })); + } + + private async findKeyRows( service: ReturnType, spreadsheetId: string, - sheetNames: string[], + sheetName: string, keyColumn: string, startRow: number - ): Promise> { - const keyLocations = new Map(); - - for (const sheetName of sheetNames) { - const response = await service.spreadsheets.values.get({ - spreadsheetId, - range: `${sheetName}!${keyColumn}${startRow}:${keyColumn}`, - }); - - const keys = response.data.values ?? []; - keys.forEach((row, index) => { - const key = row[0]; - if (!key) { - return; - } + ): Promise> { + const response = await service.spreadsheets.values.get({ + spreadsheetId, + range: `${sheetName}!${keyColumn}${startRow}:${keyColumn}`, + }); - const location: KeyLocation = { - sheetName, - rowIndex: startRow + index, - }; + const keyRows = new Map(); + const keys = response.data.values ?? []; + keys.forEach((row, index) => { + const key = row[0]; + if (!key) { + return; + } - const existing = keyLocations.get(key); - if (existing) { - throw new Error( - `Duplicate key "${key}" found in sheets "${existing.sheetName}" and "${sheetName}"` - ); - } + if (keyRows.has(key)) { + throw new Error(`Duplicate key "${key}" found in sheet "${sheetName}"`); + } - keyLocations.set(key, location); - }); - } + keyRows.set(key, startRow + index); + }); - return keyLocations; + return keyRows; } async exportAsXlsx(fileId: string, output: string, mimeType: string) { @@ -189,3 +202,11 @@ export class Drive { }); } } + +function columnToIndex(column: string): number { + let index = 0; + for (const char of column.toUpperCase()) { + index = index * 26 + (char.charCodeAt(0) - 64); + } + return index - 1; +} diff --git a/src/index.ts b/src/index.ts index 9aaa461..1c51d9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import colors from "colors/safe"; import { program } from "commander"; +import { WordingConfig } from "./config/WordingConfig"; import { loadConfiguration } from "./config/WordingConfigLoader"; import { AngularJsonWordingExporter } from "./exporters/AngularJsonWordingExporter"; import { FlatJsonWordingExporter } from "./exporters/FlatJsonWordingExportter"; @@ -24,6 +25,10 @@ program "--set ", "update wording line(s) in remote Google Sheet by key (key followed by one value per language; repeat the pattern for multiple lines)", ) + .option( + "--sheet ", + "target sheet name for --add / --set (default: first entry in sheetNames)", + ) .option("--config ", "wording config path", "wording_config.json") .option("--upgrade", "upgrade wording from remote Google Sheet") .option("--update", "update wording from local xlsx") @@ -59,6 +64,14 @@ function printReport(language: string, result: WordingResult): void { const options = program.opts(); +function sheetWriteConfig(config: WordingConfig) { + return { + keyColumn: config.keyColumn, + languages: config.languages, + sheetStartIndex: config.sheetStartIndex, + }; +} + const scopes = [ "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file", @@ -72,20 +85,26 @@ loadConfiguration(options.config).then(async (config) => { if (options.add) { const lines = parseWordingLines(options.add, config.languages.length); + const sheetName = options.sheet ?? config.sheetNames[0]; + const auth = await new GoogleAuth().authorize(config.credentials, scopes); - await new Drive(auth).addLines(config.sheetId, config.sheetNames[0], lines); + await new Drive(auth).addLines( + config.sheetId, + sheetName, + lines, + sheetWriteConfig(config), + ); } if (options.set) { const lines = parseWordingLines(options.set, config.languages.length); + const sheetName = options.sheet ?? config.sheetNames[0]; const auth = await new GoogleAuth().authorize(config.credentials, scopes); await new Drive(auth).updateLines( config.sheetId, - config.sheetNames, - config.keyColumn, - config.sheetStartIndex, - config.languages, + sheetName, lines, + sheetWriteConfig(config), ); }