gtx-cli 2.0.15 → 2.0.17-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.0.16
4
+
5
+ ### Patch Changes
6
+
7
+ - [#512](https://github.com/generaltranslation/gt/pull/512) [`7c01ee8`](https://github.com/generaltranslation/gt/commit/7c01ee8af1e882d222fc3b0224b17f459ec5243b) Thanks [@brian-lou](https://github.com/brian-lou)! - Add new CLI command 'upload', add additional transform options for file translations
8
+
3
9
  ## 2.0.15
4
10
 
5
11
  ### Patch Changes
@@ -180,6 +180,7 @@ async function checkTranslationDeployment(fileQueryData, downloadStatus, spinner
180
180
  const outputPath = resolveOutputPath(fileName, locale);
181
181
  return {
182
182
  translationId,
183
+ inputPath: fileName,
183
184
  outputPath,
184
185
  locale,
185
186
  fileLocale: `${fileName}:${locale}`,
@@ -187,9 +188,10 @@ async function checkTranslationDeployment(fileQueryData, downloadStatus, spinner
187
188
  });
188
189
  // Use batch download if there are multiple files
189
190
  if (batchFiles.length > 1) {
190
- const batchResult = await downloadFileBatch(batchFiles.map(({ translationId, outputPath, locale }) => ({
191
+ const batchResult = await downloadFileBatch(batchFiles.map(({ translationId, outputPath, inputPath, locale }) => ({
191
192
  translationId,
192
193
  outputPath,
194
+ inputPath,
193
195
  locale,
194
196
  })), options);
195
197
  // Process results
@@ -206,7 +208,7 @@ async function checkTranslationDeployment(fileQueryData, downloadStatus, spinner
206
208
  else if (batchFiles.length === 1) {
207
209
  // For a single file, use the original downloadFile method
208
210
  const file = batchFiles[0];
209
- const result = await downloadFile(file.translationId, file.outputPath, file.locale, options);
211
+ const result = await downloadFile(file.translationId, file.outputPath, file.inputPath, file.locale, options);
210
212
  if (result) {
211
213
  downloadStatus.downloaded.add(file.fileLocale);
212
214
  }
@@ -6,4 +6,4 @@ import { Settings } from '../types/index.js';
6
6
  * @param maxRetries - The maximum number of retries to attempt
7
7
  * @param retryDelay - The delay between retries in milliseconds
8
8
  */
9
- export declare function downloadFile(translationId: string, outputPath: string, locale: string, options: Settings, maxRetries?: number, retryDelay?: number): Promise<boolean>;
9
+ export declare function downloadFile(translationId: string, outputPath: string, inputPath: string, locale: string, options: Settings, maxRetries?: number, retryDelay?: number): Promise<boolean>;
@@ -12,7 +12,7 @@ import { TextDecoder } from 'node:util';
12
12
  * @param maxRetries - The maximum number of retries to attempt
13
13
  * @param retryDelay - The delay between retries in milliseconds
14
14
  */
15
- export async function downloadFile(translationId, outputPath, locale, options, maxRetries = 3, retryDelay = 1000) {
15
+ export async function downloadFile(translationId, outputPath, inputPath, locale, options, maxRetries = 3, retryDelay = 1000) {
16
16
  let retries = 0;
17
17
  while (retries <= maxRetries) {
18
18
  try {
@@ -29,7 +29,7 @@ export async function downloadFile(translationId, outputPath, locale, options, m
29
29
  if (jsonSchema) {
30
30
  const originalContent = fs.readFileSync(outputPath, 'utf8');
31
31
  if (originalContent) {
32
- data = mergeJson(originalContent, outputPath, options.options, [
32
+ data = mergeJson(originalContent, inputPath, options.options, [
33
33
  {
34
34
  translatedContent: data,
35
35
  targetLocale: locale,
@@ -2,6 +2,7 @@ import { Settings } from '../types/index.js';
2
2
  export type BatchedFiles = Array<{
3
3
  translationId: string;
4
4
  outputPath: string;
5
+ inputPath: string;
5
6
  locale: string;
6
7
  }>;
7
8
  export type DownloadFileBatchResult = {
@@ -17,6 +17,7 @@ export async function downloadFileBatch(files, options, maxRetries = 3, retryDel
17
17
  const result = { successful: [], failed: [] };
18
18
  // Create a map of translationId to outputPath for easier lookup
19
19
  const outputPathMap = new Map(files.map((file) => [file.translationId, file.outputPath]));
20
+ const inputPathMap = new Map(files.map((file) => [file.translationId, file.inputPath]));
20
21
  const localeMap = new Map(files.map((file) => [file.translationId, file.locale]));
21
22
  while (retries <= maxRetries) {
22
23
  try {
@@ -28,8 +29,9 @@ export async function downloadFileBatch(files, options, maxRetries = 3, retryDel
28
29
  try {
29
30
  const translationId = file.id;
30
31
  const outputPath = outputPathMap.get(translationId);
32
+ const inputPath = inputPathMap.get(translationId);
31
33
  const locale = localeMap.get(translationId);
32
- if (!outputPath) {
34
+ if (!outputPath || !inputPath) {
33
35
  logWarning(`No output path found for file: ${translationId}`);
34
36
  result.failed.push(translationId);
35
37
  continue;
@@ -41,11 +43,11 @@ export async function downloadFileBatch(files, options, maxRetries = 3, retryDel
41
43
  }
42
44
  let data = file.data;
43
45
  if (options.options?.jsonSchema && locale) {
44
- const jsonSchema = validateJsonSchema(options.options, outputPath);
46
+ const jsonSchema = validateJsonSchema(options.options, inputPath);
45
47
  if (jsonSchema) {
46
- const originalContent = fs.readFileSync(outputPath, 'utf8');
48
+ const originalContent = fs.readFileSync(inputPath, 'utf8');
47
49
  if (originalContent) {
48
- data = mergeJson(originalContent, outputPath, options.options, [
50
+ data = mergeJson(originalContent, inputPath, options.options, [
49
51
  {
50
52
  translatedContent: file.data,
51
53
  targetLocale: locale,
@@ -0,0 +1,26 @@
1
+ import { Settings } from '../types/index.js';
2
+ import { DataFormat, FileFormat } from '../types/data.js';
3
+ export type FileUpload = {
4
+ content: string;
5
+ fileName: string;
6
+ fileFormat: FileFormat;
7
+ dataFormat?: DataFormat;
8
+ locale: string;
9
+ };
10
+ export type UploadData = {
11
+ data: {
12
+ source: FileUpload;
13
+ translations: FileUpload[];
14
+ }[];
15
+ sourceLocale: string;
16
+ };
17
+ /**
18
+ * Uploads multiple files to the API
19
+ * @param files - Array of file objects to upload
20
+ * @param options - The options for the API call
21
+ * @returns The uploaded content or version ID
22
+ */
23
+ export declare function uploadFiles(files: {
24
+ source: FileUpload;
25
+ translations: FileUpload[];
26
+ }[], options: Settings): Promise<Response>;
@@ -0,0 +1,46 @@
1
+ import chalk from 'chalk';
2
+ import { createSpinner, exit, logMessage } from '../console/logging.js';
3
+ /**
4
+ * Uploads multiple files to the API
5
+ * @param files - Array of file objects to upload
6
+ * @param options - The options for the API call
7
+ * @returns The uploaded content or version ID
8
+ */
9
+ export async function uploadFiles(files, options) {
10
+ logMessage(chalk.cyan('Files to upload:') +
11
+ '\n' +
12
+ files
13
+ .map((file) => ` - ${chalk.bold(file.source.fileName)} -> ${file.translations
14
+ .map((t) => t.locale)
15
+ .join(', ')}`)
16
+ .join('\n'));
17
+ const spinner = createSpinner('dots');
18
+ spinner.start(`Uploading ${files.length} file${files.length !== 1 ? 's' : ''} to General Translation...`);
19
+ const uploadData = {
20
+ data: files.map((file) => ({
21
+ source: file.source,
22
+ translations: file.translations,
23
+ })),
24
+ sourceLocale: options.defaultLocale,
25
+ };
26
+ try {
27
+ const response = await fetch(`${options.baseUrl}/v1/project/files/upload`, {
28
+ method: 'POST',
29
+ body: JSON.stringify(uploadData),
30
+ headers: {
31
+ 'Content-Type': 'application/json',
32
+ 'x-gt-api-key': options.apiKey,
33
+ 'x-gt-project-id': options.projectId,
34
+ },
35
+ });
36
+ if (!response.ok) {
37
+ throw new Error(`Failed to upload files: ${response.statusText} (${response.status})`);
38
+ }
39
+ spinner.stop(chalk.green('Files uploaded successfully'));
40
+ return response;
41
+ }
42
+ catch (error) {
43
+ spinner.stop(chalk.red('An unexpected error occurred while uploading files'));
44
+ exit(1);
45
+ }
46
+ }
@@ -1,5 +1,11 @@
1
1
  import { Command } from 'commander';
2
2
  import { Settings, SupportedLibraries, SetupOptions } from '../types/index.js';
3
+ export type UploadOptions = {
4
+ config?: string;
5
+ apiKey?: string;
6
+ projectId?: string;
7
+ defaultLocale?: string;
8
+ };
3
9
  export type TranslateOptions = {
4
10
  config?: string;
5
11
  defaultLocale?: string;
@@ -23,10 +29,12 @@ export declare class BaseCLI {
23
29
  init(): void;
24
30
  execute(): void;
25
31
  protected setupGTCommand(): void;
32
+ protected setupUploadCommand(): void;
26
33
  protected setupLoginCommand(): void;
27
34
  protected setupInitCommand(): void;
28
35
  protected setupConfigureCommand(): void;
29
36
  protected setupSetupCommand(): void;
37
+ protected handleUploadCommand(settings: Settings & UploadOptions): Promise<void>;
30
38
  protected handleGenericTranslate(settings: Settings & TranslateOptions): Promise<void>;
31
39
  protected handleSetupReactCommand(options: SetupOptions): Promise<void>;
32
40
  protected handleInitCommand(ranReactSetup: boolean): Promise<void>;
package/dist/cli/base.js CHANGED
@@ -18,6 +18,7 @@ import localizeStaticUrls from '../utils/localizeStaticUrls.js';
18
18
  import flattenJsonFiles from '../utils/flattenJsonFiles.js';
19
19
  import localizeStaticImports from '../utils/localizeStaticImports.js';
20
20
  import copyFile from '../fs/copyFile.js';
21
+ import { upload } from '../formats/files/upload.js';
21
22
  export class BaseCLI {
22
23
  library;
23
24
  additionalModules;
@@ -30,6 +31,7 @@ export class BaseCLI {
30
31
  this.setupInitCommand();
31
32
  this.setupConfigureCommand();
32
33
  this.setupSetupCommand();
34
+ this.setupUploadCommand();
33
35
  this.setupLoginCommand();
34
36
  }
35
37
  // Init is never called in a child class
@@ -65,6 +67,22 @@ export class BaseCLI {
65
67
  endCommand('Done!');
66
68
  });
67
69
  }
70
+ setupUploadCommand() {
71
+ this.program
72
+ .command('upload')
73
+ .description('Upload source files and translations to the General Translation platform')
74
+ .option('-c, --config <path>', 'Filepath to config file, by default gt.config.json', findFilepath(['gt.config.json']))
75
+ .option('--api-key <key>', 'API key for General Translation cloud service')
76
+ .option('--project-id <id>', 'Project ID for the translation service')
77
+ .option('--default-language, --default-locale <locale>', 'Default locale (e.g., en)')
78
+ .action(async (initOptions) => {
79
+ displayHeader('Starting upload...');
80
+ const settings = await generateSettings(initOptions);
81
+ const options = { ...initOptions, ...settings };
82
+ await this.handleUploadCommand(options);
83
+ endCommand('Done!');
84
+ });
85
+ }
68
86
  setupLoginCommand() {
69
87
  this.program
70
88
  .command('auth')
@@ -155,6 +173,30 @@ See the docs for more information: https://generaltranslation.com/docs/react/tut
155
173
  endCommand("Done! Take advantage of all of General Translation's features by signing up for a free account! https://generaltranslation.com/signup");
156
174
  });
157
175
  }
176
+ async handleUploadCommand(settings) {
177
+ // dataFormat for JSONs
178
+ let dataFormat;
179
+ if (this.library === 'next-intl') {
180
+ dataFormat = 'ICU';
181
+ }
182
+ else if (this.library === 'i18next') {
183
+ if (this.additionalModules.includes('i18next-icu')) {
184
+ dataFormat = 'ICU';
185
+ }
186
+ else {
187
+ dataFormat = 'I18NEXT';
188
+ }
189
+ }
190
+ else {
191
+ dataFormat = 'JSX';
192
+ }
193
+ if (!settings.files) {
194
+ return;
195
+ }
196
+ const { resolvedPaths: sourceFiles, placeholderPaths, transformPaths, } = settings.files;
197
+ // Process all file types at once with a single call
198
+ await upload(sourceFiles, placeholderPaths, transformPaths, dataFormat, settings);
199
+ }
158
200
  async handleGenericTranslate(settings) {
159
201
  // dataFormat for JSONs
160
202
  let dataFormat;
@@ -0,0 +1,10 @@
1
+ import { ResolvedFiles, TransformFiles } from '../../types/index.js';
2
+ /**
3
+ * Creates a mapping between source files and their translated counterparts for each locale
4
+ * @param filePaths - Resolved file paths for different file types
5
+ * @param placeholderPaths - Placeholder paths for translated files
6
+ * @param transformPaths - Transform paths for file naming
7
+ * @param locales - List of locales to create a mapping for
8
+ * @returns A mapping between source files and their translated counterparts for each locale, in the form of relative paths
9
+ */
10
+ export declare function createFileMapping(filePaths: ResolvedFiles, placeholderPaths: ResolvedFiles, transformPaths: TransformFiles, targetLocales: string[], defaultLocale: string): Record<string, Record<string, string>>;
@@ -0,0 +1,76 @@
1
+ import { SUPPORTED_FILE_EXTENSIONS } from '../files/supportedFiles.js';
2
+ import { resolveLocaleFiles } from '../../fs/config/parseFilesConfig.js';
3
+ import path from 'node:path';
4
+ import { getRelative } from '../../fs/findFilepath.js';
5
+ import { getLocaleProperties } from 'generaltranslation';
6
+ import { replaceLocalePlaceholders } from '../utils.js';
7
+ /**
8
+ * Creates a mapping between source files and their translated counterparts for each locale
9
+ * @param filePaths - Resolved file paths for different file types
10
+ * @param placeholderPaths - Placeholder paths for translated files
11
+ * @param transformPaths - Transform paths for file naming
12
+ * @param locales - List of locales to create a mapping for
13
+ * @returns A mapping between source files and their translated counterparts for each locale, in the form of relative paths
14
+ */
15
+ export function createFileMapping(filePaths, placeholderPaths, transformPaths, targetLocales, defaultLocale) {
16
+ const fileMapping = {};
17
+ for (const locale of targetLocales) {
18
+ const translatedPaths = resolveLocaleFiles(placeholderPaths, locale);
19
+ const localeMapping = {};
20
+ // Process each file type
21
+ for (const typeIndex of SUPPORTED_FILE_EXTENSIONS) {
22
+ if (!filePaths[typeIndex] || !translatedPaths[typeIndex])
23
+ continue;
24
+ const sourcePaths = filePaths[typeIndex];
25
+ let translatedFiles = translatedPaths[typeIndex];
26
+ if (!translatedFiles)
27
+ continue;
28
+ const transformPath = transformPaths[typeIndex];
29
+ if (transformPath) {
30
+ if (typeof transformPath === 'string') {
31
+ translatedFiles = translatedFiles.map((filePath) => {
32
+ const directory = path.dirname(filePath);
33
+ const fileName = path.basename(filePath);
34
+ const baseName = fileName.split('.')[0];
35
+ const transformedFileName = transformPath
36
+ .replace('*', baseName)
37
+ .replace('[locale]', locale);
38
+ return path.join(directory, transformedFileName);
39
+ });
40
+ }
41
+ else {
42
+ // transformPath is an object
43
+ const targetLocaleProperties = getLocaleProperties(locale);
44
+ const defaultLocaleProperties = getLocaleProperties(defaultLocale);
45
+ if (!transformPath.replace ||
46
+ typeof transformPath.replace !== 'string') {
47
+ continue;
48
+ }
49
+ // Replace all locale property placeholders
50
+ const replaceString = replaceLocalePlaceholders(transformPath.replace, targetLocaleProperties);
51
+ translatedFiles = translatedFiles.map((filePath) => {
52
+ let relativePath = getRelative(filePath);
53
+ if (transformPath.match &&
54
+ typeof transformPath.match === 'string') {
55
+ // Replace locale placeholders in the match string using defaultLocale properties
56
+ let matchString = transformPath.match;
57
+ matchString = replaceLocalePlaceholders(matchString, defaultLocaleProperties);
58
+ relativePath = relativePath.replace(new RegExp(matchString, 'g'), replaceString);
59
+ }
60
+ else {
61
+ relativePath = replaceString;
62
+ }
63
+ return path.resolve(relativePath);
64
+ });
65
+ }
66
+ }
67
+ for (let i = 0; i < sourcePaths.length; i++) {
68
+ const sourceFile = getRelative(sourcePaths[i]);
69
+ const translatedFile = getRelative(translatedFiles[i]);
70
+ localeMapping[sourceFile] = translatedFile;
71
+ }
72
+ }
73
+ fileMapping[locale] = localeMapping;
74
+ }
75
+ return fileMapping;
76
+ }
@@ -11,7 +11,3 @@ import { TranslateOptions } from '../../cli/base.js';
11
11
  * @returns Promise that resolves when translation is complete
12
12
  */
13
13
  export declare function translateFiles(filePaths: ResolvedFiles, placeholderPaths: ResolvedFiles, transformPaths: TransformFiles, dataFormat: DataFormat | undefined, options: Settings & TranslateOptions): Promise<void>;
14
- /**
15
- * Creates a mapping between source files and their translated counterparts for each locale
16
- */
17
- export declare function createFileMapping(filePaths: ResolvedFiles, placeholderPaths: ResolvedFiles, transformPaths: TransformFiles, locales: string[]): Record<string, Record<string, string>>;
@@ -2,15 +2,14 @@ import { checkFileTranslations } from '../../api/checkFileTranslations.js';
2
2
  import { sendFiles } from '../../api/sendFiles.js';
3
3
  import { noSupportedFormatError, noLocalesError, noDefaultLocaleError, noApiKeyError, noProjectIdError, devApiKeyError, } from '../../console/index.js';
4
4
  import { logErrorAndExit, createSpinner, logError, logSuccess, } from '../../console/logging.js';
5
- import { resolveLocaleFiles } from '../../fs/config/parseFilesConfig.js';
6
5
  import { getRelative, readFile } from '../../fs/findFilepath.js';
7
- import path from 'node:path';
8
6
  import chalk from 'chalk';
9
7
  import { downloadFile } from '../../api/downloadFile.js';
10
8
  import { downloadFileBatch } from '../../api/downloadFileBatch.js';
11
9
  import { SUPPORTED_FILE_EXTENSIONS } from './supportedFiles.js';
12
10
  import sanitizeFileContent from '../../utils/sanitizeFileContent.js';
13
11
  import { parseJson } from '../json/parseJson.js';
12
+ import { createFileMapping } from './fileMapping.js';
14
13
  const SUPPORTED_DATA_FORMATS = ['JSX', 'ICU', 'I18NEXT'];
15
14
  /**
16
15
  * Sends multiple files to the API for translation
@@ -95,7 +94,7 @@ export async function translateFiles(filePaths, placeholderPaths, transformPaths
95
94
  });
96
95
  const { data, locales, translations } = response;
97
96
  // Create file mapping for all file types
98
- const fileMapping = createFileMapping(filePaths, placeholderPaths, transformPaths, locales);
97
+ const fileMapping = createFileMapping(filePaths, placeholderPaths, transformPaths, locales, options.defaultLocale);
99
98
  // Process any translations that were already completed and returned with the initial response
100
99
  const downloadStatus = await processInitialTranslations(translations, fileMapping, options);
101
100
  // Check for remaining translations
@@ -106,44 +105,6 @@ export async function translateFiles(filePaths, placeholderPaths, transformPaths
106
105
  logErrorAndExit(`Error translating files: ${error}`);
107
106
  }
108
107
  }
109
- /**
110
- * Creates a mapping between source files and their translated counterparts for each locale
111
- */
112
- export function createFileMapping(filePaths, placeholderPaths, transformPaths, locales) {
113
- const fileMapping = {};
114
- for (const locale of locales) {
115
- const translatedPaths = resolveLocaleFiles(placeholderPaths, locale);
116
- const localeMapping = {};
117
- // Process each file type
118
- for (const typeIndex of SUPPORTED_FILE_EXTENSIONS) {
119
- if (!filePaths[typeIndex] || !translatedPaths[typeIndex])
120
- continue;
121
- const sourcePaths = filePaths[typeIndex];
122
- let translatedFiles = translatedPaths[typeIndex];
123
- if (!translatedFiles)
124
- continue;
125
- const transformPath = transformPaths[typeIndex];
126
- if (transformPath) {
127
- translatedFiles = translatedFiles.map((filePath) => {
128
- const directory = path.dirname(filePath);
129
- const fileName = path.basename(filePath);
130
- const baseName = fileName.split('.')[0];
131
- const transformedFileName = transformPath
132
- .replace('*', baseName)
133
- .replace('[locale]', locale);
134
- return path.join(directory, transformedFileName);
135
- });
136
- }
137
- for (let i = 0; i < sourcePaths.length; i++) {
138
- const sourceFile = getRelative(sourcePaths[i]);
139
- const translatedFile = getRelative(translatedFiles[i]);
140
- localeMapping[sourceFile] = translatedFile;
141
- }
142
- }
143
- fileMapping[locale] = localeMapping;
144
- }
145
- return fileMapping;
146
- }
147
108
  /**
148
109
  * Processes translations that were already completed and returned with the initial API response
149
110
  * @returns Set of downloaded file+locale combinations
@@ -171,6 +132,7 @@ async function processInitialTranslations(translations = [], fileMapping, option
171
132
  }
172
133
  return {
173
134
  translationId: id,
135
+ inputPath: fileName,
174
136
  outputPath,
175
137
  fileLocale: `${fileName}:${locale}`,
176
138
  locale,
@@ -182,9 +144,10 @@ async function processInitialTranslations(translations = [], fileMapping, option
182
144
  }
183
145
  // Use batch download if there are multiple files
184
146
  if (batchFiles.length > 1) {
185
- const batchResult = await downloadFileBatch(batchFiles.map(({ translationId, outputPath, locale }) => ({
147
+ const batchResult = await downloadFileBatch(batchFiles.map(({ translationId, outputPath, inputPath, locale }) => ({
186
148
  translationId,
187
149
  outputPath,
150
+ inputPath,
188
151
  locale,
189
152
  })), options);
190
153
  // Process results
@@ -201,7 +164,7 @@ async function processInitialTranslations(translations = [], fileMapping, option
201
164
  else if (batchFiles.length === 1) {
202
165
  // For a single file, use the original downloadFile method
203
166
  const file = batchFiles[0];
204
- const result = await downloadFile(file.translationId, file.outputPath, file.locale, options);
167
+ const result = await downloadFile(file.translationId, file.outputPath, file.inputPath, file.locale, options);
205
168
  if (result) {
206
169
  downloadStatus.downloaded.add(file.fileLocale);
207
170
  }
@@ -0,0 +1,13 @@
1
+ import { ResolvedFiles, Settings, TransformFiles } from '../../types/index.js';
2
+ import { DataFormat } from '../../types/data.js';
3
+ import { UploadOptions } from '../../cli/base.js';
4
+ /**
5
+ * Sends multiple files to the API for translation
6
+ * @param filePaths - Resolved file paths for different file types
7
+ * @param placeholderPaths - Placeholder paths for translated files
8
+ * @param transformPaths - Transform paths for file naming
9
+ * @param dataFormat - Format of the data within the files
10
+ * @param options - Translation options including API settings
11
+ * @returns Promise that resolves when translation is complete
12
+ */
13
+ export declare function upload(filePaths: ResolvedFiles, placeholderPaths: ResolvedFiles, transformPaths: TransformFiles, dataFormat: DataFormat | undefined, options: Settings & UploadOptions): Promise<void>;
@@ -0,0 +1,192 @@
1
+ import { noSupportedFormatError, noDefaultLocaleError, noApiKeyError, noProjectIdError, devApiKeyError, } from '../../console/index.js';
2
+ import { logErrorAndExit, createSpinner, logError, } from '../../console/logging.js';
3
+ import { getRelative, readFile } from '../../fs/findFilepath.js';
4
+ import chalk from 'chalk';
5
+ import { downloadFile } from '../../api/downloadFile.js';
6
+ import { downloadFileBatch } from '../../api/downloadFileBatch.js';
7
+ import { SUPPORTED_FILE_EXTENSIONS } from './supportedFiles.js';
8
+ import sanitizeFileContent from '../../utils/sanitizeFileContent.js';
9
+ import { parseJson } from '../json/parseJson.js';
10
+ import { uploadFiles } from '../../api/uploadFiles.js';
11
+ import { existsSync, readFileSync } from 'node:fs';
12
+ import { createFileMapping } from './fileMapping.js';
13
+ const SUPPORTED_DATA_FORMATS = ['JSX', 'ICU', 'I18NEXT'];
14
+ /**
15
+ * Sends multiple files to the API for translation
16
+ * @param filePaths - Resolved file paths for different file types
17
+ * @param placeholderPaths - Placeholder paths for translated files
18
+ * @param transformPaths - Transform paths for file naming
19
+ * @param dataFormat - Format of the data within the files
20
+ * @param options - Translation options including API settings
21
+ * @returns Promise that resolves when translation is complete
22
+ */
23
+ export async function upload(filePaths, placeholderPaths, transformPaths, dataFormat = 'JSX', options) {
24
+ // Collect all files to translate
25
+ const allFiles = [];
26
+ const additionalOptions = options.options || {};
27
+ // Process JSON files
28
+ if (filePaths.json) {
29
+ if (!SUPPORTED_DATA_FORMATS.includes(dataFormat)) {
30
+ logErrorAndExit(noSupportedFormatError);
31
+ }
32
+ const jsonFiles = filePaths.json.map((filePath) => {
33
+ const content = readFile(filePath);
34
+ const parsedJson = parseJson(content, filePath, additionalOptions, options.defaultLocale);
35
+ const relativePath = getRelative(filePath);
36
+ return {
37
+ content: parsedJson,
38
+ fileName: relativePath,
39
+ fileFormat: 'JSON',
40
+ dataFormat,
41
+ locale: options.defaultLocale,
42
+ };
43
+ });
44
+ allFiles.push(...jsonFiles);
45
+ }
46
+ for (const fileType of SUPPORTED_FILE_EXTENSIONS) {
47
+ if (fileType === 'json')
48
+ continue;
49
+ if (filePaths[fileType]) {
50
+ const files = filePaths[fileType].map((filePath) => {
51
+ const content = readFile(filePath);
52
+ const sanitizedContent = sanitizeFileContent(content);
53
+ const relativePath = getRelative(filePath);
54
+ return {
55
+ content: sanitizedContent,
56
+ fileName: relativePath,
57
+ fileFormat: fileType.toUpperCase(),
58
+ dataFormat,
59
+ locale: options.defaultLocale,
60
+ };
61
+ });
62
+ allFiles.push(...files);
63
+ }
64
+ }
65
+ if (allFiles.length === 0) {
66
+ logError('No files to upload were found. Please check your configuration and try again.');
67
+ return;
68
+ }
69
+ if (!options.defaultLocale) {
70
+ logErrorAndExit(noDefaultLocaleError);
71
+ }
72
+ if (!options.apiKey) {
73
+ logErrorAndExit(noApiKeyError);
74
+ }
75
+ if (options.apiKey.startsWith('gtx-dev-')) {
76
+ logErrorAndExit(devApiKeyError);
77
+ }
78
+ if (!options.projectId) {
79
+ logErrorAndExit(noProjectIdError);
80
+ }
81
+ const locales = options.locales || [];
82
+ // Create file mapping for all file types
83
+ const fileMapping = createFileMapping(filePaths, placeholderPaths, transformPaths, locales, options.defaultLocale);
84
+ // construct object
85
+ const uploadData = allFiles.map((file) => {
86
+ const encodedContent = Buffer.from(file.content).toString('base64');
87
+ const sourceFile = {
88
+ content: encodedContent,
89
+ fileName: file.fileName,
90
+ fileFormat: file.fileFormat,
91
+ dataFormat: file.dataFormat,
92
+ locale: file.locale,
93
+ };
94
+ const translations = [];
95
+ for (const locale of locales) {
96
+ const translatedFileName = fileMapping[locale][file.fileName];
97
+ if (translatedFileName && existsSync(translatedFileName)) {
98
+ const translatedContent = readFileSync(translatedFileName, 'utf8');
99
+ const encodedTranslatedContent = Buffer.from(translatedContent).toString('base64');
100
+ translations.push({
101
+ content: encodedTranslatedContent,
102
+ fileName: translatedFileName,
103
+ fileFormat: file.fileFormat,
104
+ dataFormat: file.dataFormat,
105
+ locale,
106
+ });
107
+ }
108
+ }
109
+ return {
110
+ source: sourceFile,
111
+ translations,
112
+ };
113
+ });
114
+ try {
115
+ // Send all files in a single API call
116
+ const response = await uploadFiles(uploadData, options);
117
+ }
118
+ catch (error) {
119
+ logErrorAndExit(`Error uploading files: ${error}`);
120
+ }
121
+ }
122
+ /**
123
+ * Processes translations that were already completed and returned with the initial API response
124
+ * @returns Set of downloaded file+locale combinations
125
+ */
126
+ async function processInitialTranslations(translations = [], fileMapping, options) {
127
+ const downloadStatus = {
128
+ downloaded: new Set(),
129
+ failed: new Set(),
130
+ };
131
+ if (!translations || translations.length === 0) {
132
+ return downloadStatus;
133
+ }
134
+ // Filter for ready translations
135
+ const readyTranslations = translations.filter((translation) => translation.isReady && translation.fileName);
136
+ if (readyTranslations.length > 0) {
137
+ const spinner = createSpinner('dots');
138
+ spinner.start('Downloading translations...');
139
+ // Prepare batch download data
140
+ const batchFiles = readyTranslations
141
+ .map((translation) => {
142
+ const { locale, fileName, id } = translation;
143
+ const outputPath = fileMapping[locale][fileName];
144
+ if (!outputPath) {
145
+ return null;
146
+ }
147
+ return {
148
+ translationId: id,
149
+ outputPath,
150
+ inputPath: fileName,
151
+ fileLocale: `${fileName}:${locale}`,
152
+ locale,
153
+ };
154
+ })
155
+ .filter(Boolean);
156
+ if (batchFiles.length === 0 || batchFiles[0] === null) {
157
+ return downloadStatus;
158
+ }
159
+ // Use batch download if there are multiple files
160
+ if (batchFiles.length > 1) {
161
+ const batchResult = await downloadFileBatch(batchFiles.map(({ translationId, outputPath, inputPath, locale }) => ({
162
+ translationId,
163
+ outputPath,
164
+ inputPath,
165
+ locale,
166
+ })), options);
167
+ // Process results
168
+ batchFiles.forEach((file) => {
169
+ const { translationId, fileLocale } = file;
170
+ if (batchResult.successful.includes(translationId)) {
171
+ downloadStatus.downloaded.add(fileLocale);
172
+ }
173
+ else if (batchResult.failed.includes(translationId)) {
174
+ downloadStatus.failed.add(fileLocale);
175
+ }
176
+ });
177
+ }
178
+ else if (batchFiles.length === 1) {
179
+ // For a single file, use the original downloadFile method
180
+ const file = batchFiles[0];
181
+ const result = await downloadFile(file.translationId, file.outputPath, file.inputPath, file.locale, options);
182
+ if (result) {
183
+ downloadStatus.downloaded.add(file.fileLocale);
184
+ }
185
+ else {
186
+ downloadStatus.failed.add(file.fileLocale);
187
+ }
188
+ }
189
+ spinner.stop(chalk.green('Downloaded cached translations'));
190
+ }
191
+ return downloadStatus;
192
+ }
@@ -1,5 +1,5 @@
1
1
  import { AdditionalOptions, SourceObjectOptions } from '../../types/index.js';
2
- export declare function mergeJson(originalContent: string, filePath: string, options: AdditionalOptions, targets: {
2
+ export declare function mergeJson(originalContent: string, inputPath: string, options: AdditionalOptions, targets: {
3
3
  translatedContent: string;
4
4
  targetLocale: string;
5
5
  }[], defaultLocale: string): string[];
@@ -3,8 +3,9 @@ import { exit, logError, logWarning } from '../../console/logging.js';
3
3
  import { findMatchingItemArray, findMatchingItemObject, generateSourceObjectPointers, getSourceObjectOptionsArray, validateJsonSchema, } from './utils.js';
4
4
  import { JSONPath } from 'jsonpath-plus';
5
5
  import { getLocaleProperties } from 'generaltranslation';
6
- export function mergeJson(originalContent, filePath, options, targets, defaultLocale) {
7
- const jsonSchema = validateJsonSchema(options, filePath);
6
+ import { replaceLocalePlaceholders } from '../utils.js';
7
+ export function mergeJson(originalContent, inputPath, options, targets, defaultLocale) {
8
+ const jsonSchema = validateJsonSchema(options, inputPath);
8
9
  if (!jsonSchema) {
9
10
  return targets.map((target) => target.translatedContent);
10
11
  }
@@ -13,7 +14,7 @@ export function mergeJson(originalContent, filePath, options, targets, defaultLo
13
14
  originalJson = JSON.parse(originalContent);
14
15
  }
15
16
  catch {
16
- logError(`Invalid JSON file: ${filePath}`);
17
+ logError(`Invalid JSON file: ${inputPath}`);
17
18
  exit(1);
18
19
  }
19
20
  // Handle include
@@ -204,30 +205,6 @@ export function mergeJson(originalContent, filePath, options, targets, defaultLo
204
205
  }
205
206
  return [JSON.stringify(mergedJson, null, 2)];
206
207
  }
207
- // helper function to replace locale placeholders in a string
208
- // with the corresponding locale properties
209
- // ex: {locale} -> will be replaced with the locale code
210
- // ex: {localeName} -> will be replaced with the locale name
211
- function replaceLocalePlaceholders(string, localeProperties) {
212
- return string.replace(/\{(\w+)\}/g, (match, property) => {
213
- // Handle common aliases
214
- if (property === 'locale' || property === 'localeCode') {
215
- return localeProperties.code;
216
- }
217
- if (property === 'localeName') {
218
- return localeProperties.name;
219
- }
220
- if (property === 'localeNativeName') {
221
- return localeProperties.nativeName;
222
- }
223
- // Check if the property exists in localeProperties
224
- if (property in localeProperties) {
225
- return localeProperties[property];
226
- }
227
- // Return the original placeholder if property not found
228
- return match;
229
- });
230
- }
231
208
  /**
232
209
  * Apply transformations to the sourceItem in-place
233
210
  * @param sourceItem - The source item to apply transformations to
@@ -0,0 +1,2 @@
1
+ import { LocaleProperties } from 'generaltranslation/types';
2
+ export declare function replaceLocalePlaceholders(string: string, localeProperties: LocaleProperties): string;
@@ -0,0 +1,24 @@
1
+ // helper function to replace locale placeholders in a string
2
+ // with the corresponding locale properties
3
+ // ex: {locale} -> will be replaced with the locale code
4
+ // ex: {localeName} -> will be replaced with the locale name
5
+ export function replaceLocalePlaceholders(string, localeProperties) {
6
+ return string.replace(/\{(\w+)\}/g, (match, property) => {
7
+ // Handle common aliases
8
+ if (property === 'locale' || property === 'localeCode') {
9
+ return localeProperties.code;
10
+ }
11
+ if (property === 'localeName') {
12
+ return localeProperties.name;
13
+ }
14
+ if (property === 'localeNativeName') {
15
+ return localeProperties.nativeName;
16
+ }
17
+ // Check if the property exists in localeProperties
18
+ if (property in localeProperties) {
19
+ return localeProperties[property];
20
+ }
21
+ // Return the original placeholder if property not found
22
+ return match;
23
+ });
24
+ }
@@ -39,9 +39,11 @@ export function resolveFiles(files, locale, cwd) {
39
39
  }
40
40
  for (const fileType of SUPPORTED_FILE_EXTENSIONS) {
41
41
  // ==== TRANSFORMS ==== //
42
- if (files[fileType]?.transform &&
43
- !Array.isArray(files[fileType].transform)) {
44
- transformPaths[fileType] = files[fileType].transform;
42
+ const transform = files[fileType]?.transform;
43
+ if (transform &&
44
+ !Array.isArray(transform) &&
45
+ (typeof transform === 'string' || typeof transform === 'object')) {
46
+ transformPaths[fileType] = transform;
45
47
  }
46
48
  // ==== PLACEHOLDERS ==== //
47
49
  if (files[fileType]?.include) {
@@ -60,8 +60,12 @@ export type ResolvedFiles = {
60
60
  } & {
61
61
  gt?: string;
62
62
  };
63
+ export type TransformOption = {
64
+ match?: string;
65
+ replace: string;
66
+ };
63
67
  export type TransformFiles = {
64
- [K in SupportedFileExtension]?: string;
68
+ [K in SupportedFileExtension]?: TransformOption | string;
65
69
  };
66
70
  export type FilesOptions = {
67
71
  [K in SupportedFileExtension]?: {
@@ -119,8 +123,5 @@ export type SourceObjectOptions = {
119
123
  transform?: TransformOptions;
120
124
  };
121
125
  export type TransformOptions = {
122
- [transformPath: string]: {
123
- match?: string;
124
- replace: string;
125
- };
126
+ [transformPath: string]: TransformOption;
126
127
  };
@@ -1,4 +1,4 @@
1
- import { createFileMapping } from '../formats/files/translate.js';
1
+ import { createFileMapping } from '../formats/files/fileMapping.js';
2
2
  import fs from 'node:fs';
3
3
  export default async function flattenJsonFiles(settings) {
4
4
  if (!settings.files ||
@@ -7,7 +7,7 @@ export default async function flattenJsonFiles(settings) {
7
7
  return;
8
8
  }
9
9
  const { resolvedPaths: sourceFiles } = settings.files;
10
- const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales);
10
+ const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales, settings.defaultLocale);
11
11
  await Promise.all(Object.values(fileMapping).map(async (filesMap) => {
12
12
  const targetFiles = Object.values(filesMap).filter((path) => path.endsWith('.json'));
13
13
  await Promise.all(targetFiles.map(async (file) => {
@@ -1,5 +1,5 @@
1
1
  import * as fs from 'fs';
2
- import { createFileMapping } from '../formats/files/translate.js';
2
+ import { createFileMapping } from '../formats/files/fileMapping.js';
3
3
  import { logError } from '../console/logging.js';
4
4
  /**
5
5
  * Localizes static imports in content files.
@@ -21,7 +21,7 @@ export default async function localizeStaticImports(settings) {
21
21
  return;
22
22
  }
23
23
  const { resolvedPaths: sourceFiles } = settings.files;
24
- const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales);
24
+ const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales, settings.defaultLocale);
25
25
  // Process all file types at once with a single call
26
26
  await Promise.all(Object.entries(fileMapping).map(async ([locale, filesMap]) => {
27
27
  // Get all files that are md or mdx
@@ -1,5 +1,5 @@
1
1
  import * as fs from 'fs';
2
- import { createFileMapping } from '../formats/files/translate.js';
2
+ import { createFileMapping } from '../formats/files/fileMapping.js';
3
3
  /**
4
4
  * Localizes static urls in content files.
5
5
  * Currently only supported for md and mdx files. (/docs/ -> /[locale]/docs/)
@@ -20,7 +20,7 @@ export default async function localizeStaticUrls(settings) {
20
20
  return;
21
21
  }
22
22
  const { resolvedPaths: sourceFiles } = settings.files;
23
- const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales);
23
+ const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales, settings.defaultLocale);
24
24
  // Process all file types at once with a single call
25
25
  await Promise.all(Object.entries(fileMapping).map(async ([locale, filesMap]) => {
26
26
  // Get all files that are md or mdx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtx-cli",
3
- "version": "2.0.15",
3
+ "version": "2.0.17-alpha.1",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [