gtx-cli 2.0.15 → 2.0.16
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 +6 -0
- package/dist/api/uploadFiles.d.ts +26 -0
- package/dist/api/uploadFiles.js +46 -0
- package/dist/cli/base.d.ts +8 -0
- package/dist/cli/base.js +42 -0
- package/dist/formats/files/fileMapping.d.ts +10 -0
- package/dist/formats/files/fileMapping.js +76 -0
- package/dist/formats/files/translate.d.ts +0 -4
- package/dist/formats/files/translate.js +2 -41
- package/dist/formats/files/upload.d.ts +13 -0
- package/dist/formats/files/upload.js +190 -0
- package/dist/formats/json/mergeJson.js +1 -24
- package/dist/formats/utils.d.ts +2 -0
- package/dist/formats/utils.js +24 -0
- package/dist/fs/config/parseFilesConfig.js +5 -3
- package/dist/types/index.d.ts +6 -5
- package/dist/utils/flattenJsonFiles.js +2 -2
- package/dist/utils/localizeStaticImports.js +2 -2
- package/dist/utils/localizeStaticUrls.js +2 -2
- package/package.json +1 -1
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
|
|
@@ -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
|
+
}
|
package/dist/cli/base.d.ts
CHANGED
|
@@ -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
|
|
@@ -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,190 @@
|
|
|
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
|
+
fileLocale: `${fileName}:${locale}`,
|
|
151
|
+
locale,
|
|
152
|
+
};
|
|
153
|
+
})
|
|
154
|
+
.filter(Boolean);
|
|
155
|
+
if (batchFiles.length === 0 || batchFiles[0] === null) {
|
|
156
|
+
return downloadStatus;
|
|
157
|
+
}
|
|
158
|
+
// Use batch download if there are multiple files
|
|
159
|
+
if (batchFiles.length > 1) {
|
|
160
|
+
const batchResult = await downloadFileBatch(batchFiles.map(({ translationId, outputPath, locale }) => ({
|
|
161
|
+
translationId,
|
|
162
|
+
outputPath,
|
|
163
|
+
locale,
|
|
164
|
+
})), options);
|
|
165
|
+
// Process results
|
|
166
|
+
batchFiles.forEach((file) => {
|
|
167
|
+
const { translationId, fileLocale } = file;
|
|
168
|
+
if (batchResult.successful.includes(translationId)) {
|
|
169
|
+
downloadStatus.downloaded.add(fileLocale);
|
|
170
|
+
}
|
|
171
|
+
else if (batchResult.failed.includes(translationId)) {
|
|
172
|
+
downloadStatus.failed.add(fileLocale);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
else if (batchFiles.length === 1) {
|
|
177
|
+
// For a single file, use the original downloadFile method
|
|
178
|
+
const file = batchFiles[0];
|
|
179
|
+
const result = await downloadFile(file.translationId, file.outputPath, file.locale, options);
|
|
180
|
+
if (result) {
|
|
181
|
+
downloadStatus.downloaded.add(file.fileLocale);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
downloadStatus.failed.add(file.fileLocale);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
spinner.stop(chalk.green('Downloaded cached translations'));
|
|
188
|
+
}
|
|
189
|
+
return downloadStatus;
|
|
190
|
+
}
|
|
@@ -3,6 +3,7 @@ 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
|
+
import { replaceLocalePlaceholders } from '../utils.js';
|
|
6
7
|
export function mergeJson(originalContent, filePath, options, targets, defaultLocale) {
|
|
7
8
|
const jsonSchema = validateJsonSchema(options, filePath);
|
|
8
9
|
if (!jsonSchema) {
|
|
@@ -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,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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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/
|
|
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/
|
|
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/
|
|
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
|