gtx-cli 2.0.7 → 2.0.9
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 +15 -0
- package/dist/api/checkFileTranslations.d.ts +3 -2
- package/dist/api/checkFileTranslations.js +9 -7
- package/dist/api/downloadFile.d.ts +2 -1
- package/dist/api/downloadFile.js +20 -2
- package/dist/api/downloadFileBatch.d.ts +3 -1
- package/dist/api/downloadFileBatch.js +21 -14
- package/dist/config/generateSettings.js +17 -0
- package/dist/config/optionPresets.d.ts +2 -0
- package/dist/config/optionPresets.js +29 -0
- package/dist/config/validateSettings.d.ts +1 -1
- package/dist/console/logging.d.ts +2 -1
- package/dist/console/logging.js +4 -1
- package/dist/formats/files/translate.js +11 -10
- package/dist/formats/json/flattenJson.d.ts +14 -0
- package/dist/formats/json/flattenJson.js +64 -0
- package/dist/formats/json/mergeJson.d.ts +6 -0
- package/dist/formats/json/mergeJson.js +230 -0
- package/dist/formats/json/parseJson.d.ts +2 -0
- package/dist/formats/json/parseJson.js +97 -0
- package/dist/formats/json/utils.d.ts +26 -0
- package/dist/formats/json/utils.js +124 -0
- package/dist/fs/config/parseFilesConfig.js +1 -1
- package/dist/fs/config/setupConfig.js +2 -0
- package/dist/types/data/json.d.ts +6 -0
- package/dist/types/data/json.js +1 -0
- package/dist/types/data.d.ts +1 -1
- package/dist/types/index.d.ts +26 -0
- package/dist/utils/constants.d.ts +1 -0
- package/dist/utils/constants.js +1 -0
- package/dist/utils/localizeStaticUrls.js +13 -8
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# gtx-cli
|
|
2
2
|
|
|
3
|
+
## 2.0.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#497](https://github.com/generaltranslation/gt/pull/497) [`0f44ba0`](https://github.com/generaltranslation/gt/commit/0f44ba0eb1b31f339a43854efe4c64ca2df7e4ca) Thanks [@ErnestM1234](https://github.com/ErnestM1234)! - feat: add customizability for localize static url
|
|
8
|
+
|
|
9
|
+
## 2.0.8
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#495](https://github.com/generaltranslation/gt/pull/495) [`a7eca74`](https://github.com/generaltranslation/gt/commit/a7eca74677356b392c7c1a431f664c8e28adbf0c) Thanks [@brian-lou](https://github.com/brian-lou)! - Add support for translating arbitrary JSON files (all strings). Add support for partially translating JSON files via jsonSchema config setting. Add support for composite JSON files (where there is a single JSON containing data for all translated languages). Add support for preset jsonSchemas.
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [[`a7eca74`](https://github.com/generaltranslation/gt/commit/a7eca74677356b392c7c1a431f664c8e28adbf0c)]:
|
|
16
|
+
- generaltranslation@7.1.4
|
|
17
|
+
|
|
3
18
|
## 2.0.7
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Settings } from '../types/index.js';
|
|
1
2
|
export type CheckFileTranslationData = {
|
|
2
3
|
[key: string]: {
|
|
3
4
|
versionId: string;
|
|
@@ -14,7 +15,7 @@ export type CheckFileTranslationData = {
|
|
|
14
15
|
* @param timeoutDuration - The timeout duration for the wait in seconds
|
|
15
16
|
* @returns True if all translations are deployed, false otherwise
|
|
16
17
|
*/
|
|
17
|
-
export declare function checkFileTranslations(
|
|
18
|
+
export declare function checkFileTranslations(data: {
|
|
18
19
|
[key: string]: {
|
|
19
20
|
versionId: string;
|
|
20
21
|
fileName: string;
|
|
@@ -22,4 +23,4 @@ export declare function checkFileTranslations(projectId: string, apiKey: string,
|
|
|
22
23
|
}, locales: string[], timeoutDuration: number, resolveOutputPath: (sourcePath: string, locale: string) => string, downloadStatus: {
|
|
23
24
|
downloaded: Set<string>;
|
|
24
25
|
failed: Set<string>;
|
|
25
|
-
}): Promise<boolean>;
|
|
26
|
+
}, options: Settings): Promise<boolean>;
|
|
@@ -14,7 +14,7 @@ import { gt } from '../utils/gt.js';
|
|
|
14
14
|
* @param timeoutDuration - The timeout duration for the wait in seconds
|
|
15
15
|
* @returns True if all translations are deployed, false otherwise
|
|
16
16
|
*/
|
|
17
|
-
export async function checkFileTranslations(
|
|
17
|
+
export async function checkFileTranslations(data, locales, timeoutDuration, resolveOutputPath, downloadStatus, options) {
|
|
18
18
|
const startTime = Date.now();
|
|
19
19
|
console.log();
|
|
20
20
|
const spinner = await createOraSpinner();
|
|
@@ -22,7 +22,7 @@ export async function checkFileTranslations(projectId, apiKey, baseUrl, data, lo
|
|
|
22
22
|
// Initialize the query data
|
|
23
23
|
const fileQueryData = prepareFileQueryData(data, locales);
|
|
24
24
|
// Do first check immediately
|
|
25
|
-
const initialCheck = await checkTranslationDeployment(
|
|
25
|
+
const initialCheck = await checkTranslationDeployment(fileQueryData, downloadStatus, spinner, resolveOutputPath, options);
|
|
26
26
|
if (initialCheck) {
|
|
27
27
|
spinner.succeed(chalk.green('Files translated!'));
|
|
28
28
|
return true;
|
|
@@ -34,7 +34,7 @@ export async function checkFileTranslations(projectId, apiKey, baseUrl, data, lo
|
|
|
34
34
|
// Start the interval aligned with the original request time
|
|
35
35
|
setTimeout(() => {
|
|
36
36
|
intervalCheck = setInterval(async () => {
|
|
37
|
-
const isDeployed = await checkTranslationDeployment(
|
|
37
|
+
const isDeployed = await checkTranslationDeployment(fileQueryData, downloadStatus, spinner, resolveOutputPath, options);
|
|
38
38
|
const elapsed = Date.now() - startTime;
|
|
39
39
|
if (isDeployed || elapsed >= timeoutDuration * 1000) {
|
|
40
40
|
clearInterval(intervalCheck);
|
|
@@ -157,7 +157,7 @@ function generateStatusSuffixText(downloadStatus, fileQueryData) {
|
|
|
157
157
|
/**
|
|
158
158
|
* Checks translation status and downloads ready files
|
|
159
159
|
*/
|
|
160
|
-
async function checkTranslationDeployment(
|
|
160
|
+
async function checkTranslationDeployment(fileQueryData, downloadStatus, spinner, resolveOutputPath, options) {
|
|
161
161
|
try {
|
|
162
162
|
// Only query for files that haven't been downloaded yet
|
|
163
163
|
const currentQueryData = fileQueryData.filter((item) => !downloadStatus.downloaded.has(`${item.fileName}:${item.locale}`) &&
|
|
@@ -181,15 +181,17 @@ async function checkTranslationDeployment(baseUrl, projectId, apiKey, fileQueryD
|
|
|
181
181
|
return {
|
|
182
182
|
translationId,
|
|
183
183
|
outputPath,
|
|
184
|
+
locale,
|
|
184
185
|
fileLocale: `${fileName}:${locale}`,
|
|
185
186
|
};
|
|
186
187
|
});
|
|
187
188
|
// Use batch download if there are multiple files
|
|
188
189
|
if (batchFiles.length > 1) {
|
|
189
|
-
const batchResult = await downloadFileBatch(batchFiles.map(({ translationId, outputPath }) => ({
|
|
190
|
+
const batchResult = await downloadFileBatch(batchFiles.map(({ translationId, outputPath, locale }) => ({
|
|
190
191
|
translationId,
|
|
191
192
|
outputPath,
|
|
192
|
-
|
|
193
|
+
locale,
|
|
194
|
+
})), options);
|
|
193
195
|
// Process results
|
|
194
196
|
batchFiles.forEach((file) => {
|
|
195
197
|
const { translationId, fileLocale } = file;
|
|
@@ -204,7 +206,7 @@ async function checkTranslationDeployment(baseUrl, projectId, apiKey, fileQueryD
|
|
|
204
206
|
else if (batchFiles.length === 1) {
|
|
205
207
|
// For a single file, use the original downloadFile method
|
|
206
208
|
const file = batchFiles[0];
|
|
207
|
-
const result = await downloadFile(file.translationId, file.outputPath);
|
|
209
|
+
const result = await downloadFile(file.translationId, file.outputPath, file.locale, options);
|
|
208
210
|
if (result) {
|
|
209
211
|
downloadStatus.downloaded.add(file.fileLocale);
|
|
210
212
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Settings } from '../types/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Downloads a file from the API and saves it to a local directory
|
|
3
4
|
* @param translationId - The ID of the translation to download
|
|
@@ -5,4 +6,4 @@
|
|
|
5
6
|
* @param maxRetries - The maximum number of retries to attempt
|
|
6
7
|
* @param retryDelay - The delay between retries in milliseconds
|
|
7
8
|
*/
|
|
8
|
-
export declare function downloadFile(translationId: string, outputPath: string, maxRetries?: number, retryDelay?: number): Promise<boolean>;
|
|
9
|
+
export declare function downloadFile(translationId: string, outputPath: string, locale: string, options: Settings, maxRetries?: number, retryDelay?: number): Promise<boolean>;
|
package/dist/api/downloadFile.js
CHANGED
|
@@ -2,6 +2,9 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { logError } from '../console/logging.js';
|
|
4
4
|
import { gt } from '../utils/gt.js';
|
|
5
|
+
import { validateJsonSchema } from '../formats/json/utils.js';
|
|
6
|
+
import { mergeJson } from '../formats/json/mergeJson.js';
|
|
7
|
+
import { TextDecoder } from 'node:util';
|
|
5
8
|
/**
|
|
6
9
|
* Downloads a file from the API and saves it to a local directory
|
|
7
10
|
* @param translationId - The ID of the translation to download
|
|
@@ -9,7 +12,7 @@ import { gt } from '../utils/gt.js';
|
|
|
9
12
|
* @param maxRetries - The maximum number of retries to attempt
|
|
10
13
|
* @param retryDelay - The delay between retries in milliseconds
|
|
11
14
|
*/
|
|
12
|
-
export async function downloadFile(translationId, outputPath, maxRetries = 3, retryDelay = 1000) {
|
|
15
|
+
export async function downloadFile(translationId, outputPath, locale, options, maxRetries = 3, retryDelay = 1000) {
|
|
13
16
|
let retries = 0;
|
|
14
17
|
while (retries <= maxRetries) {
|
|
15
18
|
try {
|
|
@@ -20,8 +23,23 @@ export async function downloadFile(translationId, outputPath, maxRetries = 3, re
|
|
|
20
23
|
if (!fs.existsSync(dir)) {
|
|
21
24
|
fs.mkdirSync(dir, { recursive: true });
|
|
22
25
|
}
|
|
26
|
+
let data = new TextDecoder().decode(fileData);
|
|
27
|
+
if (options.options?.jsonSchema && locale) {
|
|
28
|
+
const jsonSchema = validateJsonSchema(options.options, outputPath);
|
|
29
|
+
if (jsonSchema) {
|
|
30
|
+
const originalContent = fs.readFileSync(outputPath, 'utf8');
|
|
31
|
+
if (originalContent) {
|
|
32
|
+
data = mergeJson(originalContent, outputPath, options.options, [
|
|
33
|
+
{
|
|
34
|
+
translatedContent: data,
|
|
35
|
+
targetLocale: locale,
|
|
36
|
+
},
|
|
37
|
+
], options.defaultLocale)[0];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
23
41
|
// Write the file to disk
|
|
24
|
-
await fs.promises.writeFile(outputPath,
|
|
42
|
+
await fs.promises.writeFile(outputPath, data);
|
|
25
43
|
return true;
|
|
26
44
|
}
|
|
27
45
|
catch (error) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { Settings } from '../types/index.js';
|
|
1
2
|
export type BatchedFiles = Array<{
|
|
2
3
|
translationId: string;
|
|
3
4
|
outputPath: string;
|
|
5
|
+
locale: string;
|
|
4
6
|
}>;
|
|
5
7
|
export type DownloadFileBatchResult = {
|
|
6
8
|
successful: string[];
|
|
@@ -13,4 +15,4 @@ export type DownloadFileBatchResult = {
|
|
|
13
15
|
* @param retryDelay - Delay between retries in milliseconds
|
|
14
16
|
* @returns Object containing successful and failed file IDs
|
|
15
17
|
*/
|
|
16
|
-
export declare function downloadFileBatch(files: BatchedFiles, maxRetries?: number, retryDelay?: number): Promise<DownloadFileBatchResult>;
|
|
18
|
+
export declare function downloadFileBatch(files: BatchedFiles, options: Settings, maxRetries?: number, retryDelay?: number): Promise<DownloadFileBatchResult>;
|
|
@@ -2,6 +2,8 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { logError, logWarning } from '../console/logging.js';
|
|
4
4
|
import { gt } from '../utils/gt.js';
|
|
5
|
+
import { validateJsonSchema } from '../formats/json/utils.js';
|
|
6
|
+
import { mergeJson } from '../formats/json/mergeJson.js';
|
|
5
7
|
/**
|
|
6
8
|
* Downloads multiple translation files in a single batch request
|
|
7
9
|
* @param files - Array of files to download with their output paths
|
|
@@ -9,12 +11,13 @@ import { gt } from '../utils/gt.js';
|
|
|
9
11
|
* @param retryDelay - Delay between retries in milliseconds
|
|
10
12
|
* @returns Object containing successful and failed file IDs
|
|
11
13
|
*/
|
|
12
|
-
export async function downloadFileBatch(files, maxRetries = 3, retryDelay = 1000) {
|
|
14
|
+
export async function downloadFileBatch(files, options, maxRetries = 3, retryDelay = 1000) {
|
|
13
15
|
let retries = 0;
|
|
14
16
|
const fileIds = files.map((file) => file.translationId);
|
|
15
17
|
const result = { successful: [], failed: [] };
|
|
16
18
|
// Create a map of translationId to outputPath for easier lookup
|
|
17
19
|
const outputPathMap = new Map(files.map((file) => [file.translationId, file.outputPath]));
|
|
20
|
+
const localeMap = new Map(files.map((file) => [file.translationId, file.locale]));
|
|
18
21
|
while (retries <= maxRetries) {
|
|
19
22
|
try {
|
|
20
23
|
// Download the files
|
|
@@ -25,6 +28,7 @@ export async function downloadFileBatch(files, maxRetries = 3, retryDelay = 1000
|
|
|
25
28
|
try {
|
|
26
29
|
const translationId = file.id;
|
|
27
30
|
const outputPath = outputPathMap.get(translationId);
|
|
31
|
+
const locale = localeMap.get(translationId);
|
|
28
32
|
if (!outputPath) {
|
|
29
33
|
logWarning(`No output path found for file: ${translationId}`);
|
|
30
34
|
result.failed.push(translationId);
|
|
@@ -35,8 +39,23 @@ export async function downloadFileBatch(files, maxRetries = 3, retryDelay = 1000
|
|
|
35
39
|
if (!fs.existsSync(dir)) {
|
|
36
40
|
fs.mkdirSync(dir, { recursive: true });
|
|
37
41
|
}
|
|
42
|
+
let data = file.data;
|
|
43
|
+
if (options.options?.jsonSchema && locale) {
|
|
44
|
+
const jsonSchema = validateJsonSchema(options.options, outputPath);
|
|
45
|
+
if (jsonSchema) {
|
|
46
|
+
const originalContent = fs.readFileSync(outputPath, 'utf8');
|
|
47
|
+
if (originalContent) {
|
|
48
|
+
data = mergeJson(originalContent, outputPath, options.options, [
|
|
49
|
+
{
|
|
50
|
+
translatedContent: file.data,
|
|
51
|
+
targetLocale: locale,
|
|
52
|
+
},
|
|
53
|
+
], options.defaultLocale)[0];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
38
57
|
// Write the file to disk
|
|
39
|
-
await fs.promises.writeFile(outputPath,
|
|
58
|
+
await fs.promises.writeFile(outputPath, data);
|
|
40
59
|
result.successful.push(translationId);
|
|
41
60
|
}
|
|
42
61
|
catch (error) {
|
|
@@ -52,18 +71,6 @@ export async function downloadFileBatch(files, maxRetries = 3, retryDelay = 1000
|
|
|
52
71
|
}
|
|
53
72
|
}
|
|
54
73
|
return result;
|
|
55
|
-
// // If we get here, the response was not OK
|
|
56
|
-
// if (retries >= maxRetries) {
|
|
57
|
-
// logError(
|
|
58
|
-
// `Failed to download files in batch. Status: ${response.status} after ${maxRetries + 1} attempts.`
|
|
59
|
-
// );
|
|
60
|
-
// // Mark all files as failed
|
|
61
|
-
// result.failed = [...fileIds];
|
|
62
|
-
// return result;
|
|
63
|
-
// }
|
|
64
|
-
// // Increment retry counter and wait before next attempt
|
|
65
|
-
// retries++;
|
|
66
|
-
// await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
67
74
|
}
|
|
68
75
|
catch (error) {
|
|
69
76
|
// If we've retried too many times, log an error and return false
|
|
@@ -11,6 +11,7 @@ import path from 'node:path';
|
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import { resolveConfig } from './resolveConfig.js';
|
|
13
13
|
import { gt } from '../utils/gt.js';
|
|
14
|
+
import { generatePreset } from './optionPresets.js';
|
|
14
15
|
export const DEFAULT_SRC_PATTERNS = [
|
|
15
16
|
'src/**/*.{js,jsx,ts,tsx}',
|
|
16
17
|
'app/**/*.{js,jsx,ts,tsx}',
|
|
@@ -59,6 +60,10 @@ export async function generateSettings(options, cwd = process.cwd()) {
|
|
|
59
60
|
gtConfig.projectId !== projectIdEnv) {
|
|
60
61
|
logErrorAndExit(`Project ID mismatch between ${chalk.green(gtConfig.projectId)} and ${chalk.green(projectIdEnv)}! Please use the same projectId in all configs.`);
|
|
61
62
|
}
|
|
63
|
+
if (options.options?.docsUrlPattern &&
|
|
64
|
+
!options.options?.docsUrlPattern.includes('[locale]')) {
|
|
65
|
+
logErrorAndExit('Failed to localize static urls: URL pattern must include "[locale]" to denote the location of the locale');
|
|
66
|
+
}
|
|
62
67
|
// merge options
|
|
63
68
|
const mergedOptions = { ...gtConfig, ...options };
|
|
64
69
|
// merge locales
|
|
@@ -91,6 +96,18 @@ export async function generateSettings(options, cwd = process.cwd()) {
|
|
|
91
96
|
mergedOptions.files = mergedOptions.files
|
|
92
97
|
? resolveFiles(mergedOptions.files, mergedOptions.defaultLocale, cwd)
|
|
93
98
|
: undefined;
|
|
99
|
+
// Add additional options if provided
|
|
100
|
+
if (mergedOptions.options && mergedOptions.options.jsonSchema) {
|
|
101
|
+
for (const fileGlob of Object.keys(mergedOptions.options.jsonSchema)) {
|
|
102
|
+
const jsonSchema = mergedOptions.options.jsonSchema[fileGlob];
|
|
103
|
+
if (jsonSchema.preset) {
|
|
104
|
+
mergedOptions.options.jsonSchema[fileGlob] = {
|
|
105
|
+
...generatePreset(jsonSchema.preset),
|
|
106
|
+
...jsonSchema,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
94
111
|
// if there's no existing config file, creates one
|
|
95
112
|
// does not include the API key to avoid exposing it
|
|
96
113
|
if (!fs.existsSync(mergedOptions.config)) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function generatePreset(preset) {
|
|
2
|
+
switch (preset) {
|
|
3
|
+
case 'mintlify':
|
|
4
|
+
// https://mintlify.com/docs/navigation
|
|
5
|
+
return {
|
|
6
|
+
composite: {
|
|
7
|
+
'$.navigation.languages': {
|
|
8
|
+
type: 'array',
|
|
9
|
+
key: '$.language',
|
|
10
|
+
include: [
|
|
11
|
+
'$..group',
|
|
12
|
+
'$..tab',
|
|
13
|
+
'$..item',
|
|
14
|
+
'$..anchor',
|
|
15
|
+
'$..dropdown',
|
|
16
|
+
],
|
|
17
|
+
transform: {
|
|
18
|
+
'$..pages[*]': {
|
|
19
|
+
match: '^{locale}/(.*)$',
|
|
20
|
+
replace: '{locale}/$1',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
default:
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -4,7 +4,8 @@ export declare function logError(message: string): void;
|
|
|
4
4
|
export declare function logSuccess(message: string): void;
|
|
5
5
|
export declare function logStep(message: string): void;
|
|
6
6
|
export declare function logMessage(message: string): void;
|
|
7
|
-
export declare function logErrorAndExit(message: string):
|
|
7
|
+
export declare function logErrorAndExit(message: string): never;
|
|
8
|
+
export declare function exit(code: number): never;
|
|
8
9
|
export declare function startCommand(message: string): void;
|
|
9
10
|
export declare function endCommand(message: string): void;
|
|
10
11
|
export declare function displayHeader(introString?: string): void;
|
package/dist/console/logging.js
CHANGED
|
@@ -22,7 +22,10 @@ export function logMessage(message) {
|
|
|
22
22
|
}
|
|
23
23
|
export function logErrorAndExit(message) {
|
|
24
24
|
log.error(message);
|
|
25
|
-
|
|
25
|
+
exit(1);
|
|
26
|
+
}
|
|
27
|
+
export function exit(code) {
|
|
28
|
+
process.exit(code);
|
|
26
29
|
}
|
|
27
30
|
// Clack prompts
|
|
28
31
|
export function startCommand(message) {
|
|
@@ -4,13 +4,13 @@ import { noSupportedFormatError, noLocalesError, noDefaultLocaleError, noApiKeyE
|
|
|
4
4
|
import { logErrorAndExit, createSpinner, logError, logSuccess, } from '../../console/logging.js';
|
|
5
5
|
import { resolveLocaleFiles } from '../../fs/config/parseFilesConfig.js';
|
|
6
6
|
import { getRelative, readFile } from '../../fs/findFilepath.js';
|
|
7
|
-
import { flattenJsonDictionary } from '../../react/utils/flattenDictionary.js';
|
|
8
7
|
import path from 'node:path';
|
|
9
8
|
import chalk from 'chalk';
|
|
10
9
|
import { downloadFile } from '../../api/downloadFile.js';
|
|
11
10
|
import { downloadFileBatch } from '../../api/downloadFileBatch.js';
|
|
12
11
|
import { SUPPORTED_FILE_EXTENSIONS } from './supportedFiles.js';
|
|
13
12
|
import sanitizeFileContent from '../../utils/sanitizeFileContent.js';
|
|
13
|
+
import { parseJson } from '../json/parseJson.js';
|
|
14
14
|
const SUPPORTED_DATA_FORMATS = ['JSX', 'ICU', 'I18NEXT'];
|
|
15
15
|
/**
|
|
16
16
|
* Sends multiple files to the API for translation
|
|
@@ -24,6 +24,7 @@ const SUPPORTED_DATA_FORMATS = ['JSX', 'ICU', 'I18NEXT'];
|
|
|
24
24
|
export async function translateFiles(filePaths, placeholderPaths, transformPaths, dataFormat = 'JSX', options) {
|
|
25
25
|
// Collect all files to translate
|
|
26
26
|
const allFiles = [];
|
|
27
|
+
const additionalOptions = options.options || {};
|
|
27
28
|
// Process JSON files
|
|
28
29
|
if (filePaths.json) {
|
|
29
30
|
if (!SUPPORTED_DATA_FORMATS.includes(dataFormat)) {
|
|
@@ -31,12 +32,10 @@ export async function translateFiles(filePaths, placeholderPaths, transformPaths
|
|
|
31
32
|
}
|
|
32
33
|
const jsonFiles = filePaths.json.map((filePath) => {
|
|
33
34
|
const content = readFile(filePath);
|
|
34
|
-
const
|
|
35
|
-
// Just to validate the JSON is valid
|
|
36
|
-
flattenJsonDictionary(json);
|
|
35
|
+
const parsedJson = parseJson(content, filePath, additionalOptions, options.defaultLocale);
|
|
37
36
|
const relativePath = getRelative(filePath);
|
|
38
37
|
return {
|
|
39
|
-
content,
|
|
38
|
+
content: parsedJson,
|
|
40
39
|
fileName: relativePath,
|
|
41
40
|
fileFormat: 'JSON',
|
|
42
41
|
dataFormat,
|
|
@@ -100,8 +99,8 @@ export async function translateFiles(filePaths, placeholderPaths, transformPaths
|
|
|
100
99
|
// Process any translations that were already completed and returned with the initial response
|
|
101
100
|
const downloadStatus = await processInitialTranslations(translations, fileMapping, options);
|
|
102
101
|
// Check for remaining translations
|
|
103
|
-
await checkFileTranslations(
|
|
104
|
-
);
|
|
102
|
+
await checkFileTranslations(data, locales, 600, (sourcePath, locale) => fileMapping[locale][sourcePath], downloadStatus, // Pass the already downloaded files to avoid duplicate requests
|
|
103
|
+
options);
|
|
105
104
|
}
|
|
106
105
|
catch (error) {
|
|
107
106
|
logErrorAndExit(`Error translating files: ${error}`);
|
|
@@ -174,6 +173,7 @@ async function processInitialTranslations(translations = [], fileMapping, option
|
|
|
174
173
|
translationId: id,
|
|
175
174
|
outputPath,
|
|
176
175
|
fileLocale: `${fileName}:${locale}`,
|
|
176
|
+
locale,
|
|
177
177
|
};
|
|
178
178
|
})
|
|
179
179
|
.filter(Boolean);
|
|
@@ -182,10 +182,11 @@ async function processInitialTranslations(translations = [], fileMapping, option
|
|
|
182
182
|
}
|
|
183
183
|
// Use batch download if there are multiple files
|
|
184
184
|
if (batchFiles.length > 1) {
|
|
185
|
-
const batchResult = await downloadFileBatch(batchFiles.map(({ translationId, outputPath }) => ({
|
|
185
|
+
const batchResult = await downloadFileBatch(batchFiles.map(({ translationId, outputPath, locale }) => ({
|
|
186
186
|
translationId,
|
|
187
187
|
outputPath,
|
|
188
|
-
|
|
188
|
+
locale,
|
|
189
|
+
})), options);
|
|
189
190
|
// Process results
|
|
190
191
|
batchFiles.forEach((file) => {
|
|
191
192
|
const { translationId, fileLocale } = file;
|
|
@@ -200,7 +201,7 @@ async function processInitialTranslations(translations = [], fileMapping, option
|
|
|
200
201
|
else if (batchFiles.length === 1) {
|
|
201
202
|
// For a single file, use the original downloadFile method
|
|
202
203
|
const file = batchFiles[0];
|
|
203
|
-
const result = await downloadFile(file.translationId, file.outputPath);
|
|
204
|
+
const result = await downloadFile(file.translationId, file.outputPath, file.locale, options);
|
|
204
205
|
if (result) {
|
|
205
206
|
downloadStatus.downloaded.add(file.fileLocale);
|
|
206
207
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flattens a JSON object according to a list of JSON paths.
|
|
3
|
+
* @param json - The JSON object to flatten
|
|
4
|
+
* @param jsonPaths - The list of JSON paths to flatten
|
|
5
|
+
* @returns A mapping of json pointers to their values
|
|
6
|
+
*/
|
|
7
|
+
export declare function flattenJson(json: any, jsonPaths: string[]): Record<string, any>;
|
|
8
|
+
/**
|
|
9
|
+
* Flattens a JSON object according to a list of JSON paths, only including strings
|
|
10
|
+
* @param json - The JSON object to flatten
|
|
11
|
+
* @param jsonPaths - The list of JSON paths to flatten
|
|
12
|
+
* @returns A mapping of json pointers to their values
|
|
13
|
+
*/
|
|
14
|
+
export declare function flattenJsonWithStringFilter(json: any, jsonPaths: string[]): Record<string, any>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
2
|
+
import { logError } from '../../console/logging.js';
|
|
3
|
+
/**
|
|
4
|
+
* Flattens a JSON object according to a list of JSON paths.
|
|
5
|
+
* @param json - The JSON object to flatten
|
|
6
|
+
* @param jsonPaths - The list of JSON paths to flatten
|
|
7
|
+
* @returns A mapping of json pointers to their values
|
|
8
|
+
*/
|
|
9
|
+
export function flattenJson(json, jsonPaths) {
|
|
10
|
+
const extractedJson = {};
|
|
11
|
+
for (const jsonPath of jsonPaths) {
|
|
12
|
+
try {
|
|
13
|
+
const results = JSONPath({
|
|
14
|
+
json,
|
|
15
|
+
path: jsonPath,
|
|
16
|
+
resultType: 'all',
|
|
17
|
+
flatten: true,
|
|
18
|
+
wrap: true,
|
|
19
|
+
});
|
|
20
|
+
if (!results || results.length === 0) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
results.forEach((result) => {
|
|
24
|
+
extractedJson[result.pointer] = result.value;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
logError(`Error with JSONPath pattern: ${jsonPath}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return extractedJson;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Flattens a JSON object according to a list of JSON paths, only including strings
|
|
35
|
+
* @param json - The JSON object to flatten
|
|
36
|
+
* @param jsonPaths - The list of JSON paths to flatten
|
|
37
|
+
* @returns A mapping of json pointers to their values
|
|
38
|
+
*/
|
|
39
|
+
export function flattenJsonWithStringFilter(json, jsonPaths) {
|
|
40
|
+
const extractedJson = {};
|
|
41
|
+
for (const jsonPath of jsonPaths) {
|
|
42
|
+
try {
|
|
43
|
+
const results = JSONPath({
|
|
44
|
+
json,
|
|
45
|
+
path: jsonPath,
|
|
46
|
+
resultType: 'all',
|
|
47
|
+
flatten: true,
|
|
48
|
+
wrap: true,
|
|
49
|
+
});
|
|
50
|
+
if (!results || results.length === 0) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
results.forEach((result) => {
|
|
54
|
+
if (typeof result.value === 'string') {
|
|
55
|
+
extractedJson[result.pointer] = result.value;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
logError(`Error with JSONPath pattern: ${jsonPath}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return extractedJson;
|
|
64
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AdditionalOptions, SourceObjectOptions } from '../../types/index.js';
|
|
2
|
+
export declare function mergeJson(originalContent: string, filePath: string, options: AdditionalOptions, targets: {
|
|
3
|
+
translatedContent: string;
|
|
4
|
+
targetLocale: string;
|
|
5
|
+
}[], defaultLocale: string): string[];
|
|
6
|
+
export declare function applyTransformations(sourceItem: any, transform: SourceObjectOptions['transform'], targetLocale: string, defaultLocale: string): void;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import JSONPointer from 'jsonpointer';
|
|
2
|
+
import { exit, logError, logWarning } from '../../console/logging.js';
|
|
3
|
+
import { findMatchingItemArray, findMatchingItemObject, generateSourceObjectPointers, getSourceObjectOptionsArray, validateJsonSchema, } from './utils.js';
|
|
4
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
5
|
+
import { getLocaleProperties } from 'generaltranslation';
|
|
6
|
+
export function mergeJson(originalContent, filePath, options, targets, defaultLocale) {
|
|
7
|
+
const jsonSchema = validateJsonSchema(options, filePath);
|
|
8
|
+
if (!jsonSchema) {
|
|
9
|
+
return targets.map((target) => target.translatedContent);
|
|
10
|
+
}
|
|
11
|
+
let originalJson;
|
|
12
|
+
try {
|
|
13
|
+
originalJson = JSON.parse(originalContent);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
logError(`Invalid JSON file: ${filePath}`);
|
|
17
|
+
exit(1);
|
|
18
|
+
}
|
|
19
|
+
// Handle include
|
|
20
|
+
if (jsonSchema.include) {
|
|
21
|
+
const output = [];
|
|
22
|
+
for (const target of targets) {
|
|
23
|
+
// Must clone the original JSON to avoid mutations
|
|
24
|
+
const mergedJson = structuredClone(originalJson);
|
|
25
|
+
const translatedJson = JSON.parse(target.translatedContent);
|
|
26
|
+
for (const [jsonPointer, translatedValue] of Object.entries(translatedJson)) {
|
|
27
|
+
try {
|
|
28
|
+
const value = JSONPointer.get(mergedJson, jsonPointer);
|
|
29
|
+
if (!value)
|
|
30
|
+
continue;
|
|
31
|
+
JSONPointer.set(mergedJson, jsonPointer, translatedValue);
|
|
32
|
+
}
|
|
33
|
+
catch (error) { }
|
|
34
|
+
}
|
|
35
|
+
output.push(JSON.stringify(mergedJson, null, 2));
|
|
36
|
+
}
|
|
37
|
+
return output;
|
|
38
|
+
}
|
|
39
|
+
if (!jsonSchema.composite) {
|
|
40
|
+
logError('No composite property found in JSON schema');
|
|
41
|
+
exit(1);
|
|
42
|
+
}
|
|
43
|
+
// Handle composite
|
|
44
|
+
// Create a deep copy of the original JSON to avoid mutations
|
|
45
|
+
const mergedJson = structuredClone(originalJson);
|
|
46
|
+
// Create mapping of sourceObjectPointer to SourceObjectOptions
|
|
47
|
+
const sourceObjectPointers = generateSourceObjectPointers(jsonSchema.composite, originalJson);
|
|
48
|
+
// Find the source object
|
|
49
|
+
for (const [sourceObjectPointer, { sourceObjectValue, sourceObjectOptions },] of Object.entries(sourceObjectPointers)) {
|
|
50
|
+
// Find the source item
|
|
51
|
+
if (sourceObjectOptions.type === 'array') {
|
|
52
|
+
// Validate type
|
|
53
|
+
if (!Array.isArray(sourceObjectValue)) {
|
|
54
|
+
logError(`Source object value is not an array at path: ${sourceObjectPointer}`);
|
|
55
|
+
exit(1);
|
|
56
|
+
}
|
|
57
|
+
// Get source item for default locale
|
|
58
|
+
const matchingDefaultLocaleItem = findMatchingItemArray(defaultLocale, sourceObjectOptions, sourceObjectPointer, sourceObjectValue);
|
|
59
|
+
if (!matchingDefaultLocaleItem) {
|
|
60
|
+
logError(`Matching sourceItem not found at path: ${sourceObjectPointer} for locale: ${defaultLocale}. Please check your JSON schema`);
|
|
61
|
+
exit(1);
|
|
62
|
+
}
|
|
63
|
+
const { sourceItem: defaultLocaleSourceItem, keyPointer: defaultLocaleKeyPointer, } = matchingDefaultLocaleItem;
|
|
64
|
+
// For each target:
|
|
65
|
+
// 1. Validate that the targetJson has a jsonPointer for the current sourceObjectPointer
|
|
66
|
+
// 2. If it does, find the source item for the target locale
|
|
67
|
+
// 3. Override the source item with the translated values
|
|
68
|
+
// 4. Apply additional mutations to the sourceItem
|
|
69
|
+
// 5. Merge the source item with the original JSON
|
|
70
|
+
for (const target of targets) {
|
|
71
|
+
const targetJson = JSON.parse(target.translatedContent);
|
|
72
|
+
// 1. Validate that the targetJson has a jsonPointer for the current sourceObjectPointer
|
|
73
|
+
if (!targetJson[sourceObjectPointer]) {
|
|
74
|
+
logWarning(`Translated JSON for locale: ${target.targetLocale} does not have a valid sourceObjectPointer: ${sourceObjectPointer}. Skipping this target`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// 2. Find the source item for the target locale
|
|
78
|
+
const matchingTargetItem = findMatchingItemArray(target.targetLocale, sourceObjectOptions, sourceObjectPointer, sourceObjectValue);
|
|
79
|
+
// If the target locale has a matching source item, use it to mutate the source item
|
|
80
|
+
// Otherwise, fallback to the default locale source item
|
|
81
|
+
const mutateSourceItem = structuredClone(defaultLocaleSourceItem);
|
|
82
|
+
const mutateSourceItemIndex = matchingTargetItem
|
|
83
|
+
? matchingTargetItem.itemIndex
|
|
84
|
+
: undefined;
|
|
85
|
+
const mutateSourceItemKeyPointer = defaultLocaleKeyPointer;
|
|
86
|
+
const { identifyingLocaleProperty: targetLocaleKeyProperty } = getSourceObjectOptionsArray(target.targetLocale, sourceObjectPointer, sourceObjectOptions);
|
|
87
|
+
// 3. Override the source item with the translated values
|
|
88
|
+
JSONPointer.set(mutateSourceItem, mutateSourceItemKeyPointer, targetLocaleKeyProperty);
|
|
89
|
+
for (const [translatedKeyJsonPointer, translatedValue,] of Object.entries(targetJson[sourceObjectPointer] || {})) {
|
|
90
|
+
try {
|
|
91
|
+
const value = JSONPointer.get(mutateSourceItem, translatedKeyJsonPointer);
|
|
92
|
+
if (!value)
|
|
93
|
+
continue;
|
|
94
|
+
JSONPointer.set(mutateSourceItem, translatedKeyJsonPointer, translatedValue);
|
|
95
|
+
}
|
|
96
|
+
catch (error) { }
|
|
97
|
+
}
|
|
98
|
+
// 4. Apply additional mutations to the sourceItem
|
|
99
|
+
applyTransformations(mutateSourceItem, sourceObjectOptions.transform, target.targetLocale, defaultLocale);
|
|
100
|
+
// 5. Merge the source item with the original JSON
|
|
101
|
+
if (mutateSourceItemIndex) {
|
|
102
|
+
sourceObjectValue[mutateSourceItemIndex] = mutateSourceItem;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
sourceObjectValue.push(mutateSourceItem);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
JSONPointer.set(mergedJson, sourceObjectPointer, sourceObjectValue);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Validate type
|
|
112
|
+
if (typeof sourceObjectValue !== 'object' || sourceObjectValue === null) {
|
|
113
|
+
logError(`Source object value is not an object at path: ${sourceObjectPointer}`);
|
|
114
|
+
exit(1);
|
|
115
|
+
}
|
|
116
|
+
// Validate localeProperty
|
|
117
|
+
const matchingDefaultLocaleItem = findMatchingItemObject(defaultLocale, sourceObjectPointer, sourceObjectOptions, sourceObjectValue);
|
|
118
|
+
// Validate source item exists
|
|
119
|
+
if (!matchingDefaultLocaleItem.sourceItem) {
|
|
120
|
+
logError(`Source item not found at path: ${sourceObjectPointer}. You must specify a source item where its key matches the default locale`);
|
|
121
|
+
exit(1);
|
|
122
|
+
}
|
|
123
|
+
const { sourceItem: defaultLocaleSourceItem } = matchingDefaultLocaleItem;
|
|
124
|
+
// For each target:
|
|
125
|
+
// 1. Validate that the targetJson has a jsonPointer for the current sourceObjectPointer
|
|
126
|
+
// 2. If it does, find the source item for the target locale
|
|
127
|
+
// 3. Override the source item with the translated values
|
|
128
|
+
// 4. Apply additional mutations to the sourceItem
|
|
129
|
+
// 5. Merge the source item with the original JSON
|
|
130
|
+
for (const target of targets) {
|
|
131
|
+
const targetJson = JSON.parse(target.translatedContent);
|
|
132
|
+
// 1. Validate that the targetJson has a jsonPointer for the current sourceObjectPointer
|
|
133
|
+
if (!targetJson[sourceObjectPointer]) {
|
|
134
|
+
logWarning(`Translated JSON for locale: ${target.targetLocale} does not have a valid sourceObjectPointer: ${sourceObjectPointer}. Skipping this target`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// 2. Find the source item for the target locale
|
|
138
|
+
const matchingTargetItem = findMatchingItemObject(target.targetLocale, sourceObjectPointer, sourceObjectOptions, sourceObjectValue);
|
|
139
|
+
// If the target locale has a matching source item, use it to mutate the source item
|
|
140
|
+
// Otherwise, fallback to the default locale source item
|
|
141
|
+
const mutateSourceItem = structuredClone(defaultLocaleSourceItem);
|
|
142
|
+
const mutateSourceItemKey = matchingTargetItem.keyParentProperty;
|
|
143
|
+
// 3. Override the source item with the translated values
|
|
144
|
+
for (const [translatedKeyJsonPointer, translatedValue,] of Object.entries(targetJson[sourceObjectPointer] || {})) {
|
|
145
|
+
try {
|
|
146
|
+
const value = JSONPointer.get(mutateSourceItem, translatedKeyJsonPointer);
|
|
147
|
+
if (!value)
|
|
148
|
+
continue;
|
|
149
|
+
JSONPointer.set(mutateSourceItem, translatedKeyJsonPointer, translatedValue);
|
|
150
|
+
}
|
|
151
|
+
catch (error) { }
|
|
152
|
+
}
|
|
153
|
+
// 4. Apply additional mutations to the sourceItem
|
|
154
|
+
applyTransformations(mutateSourceItem, sourceObjectOptions.transform, target.targetLocale, defaultLocale);
|
|
155
|
+
// 5. Merge the source item with the original JSON
|
|
156
|
+
sourceObjectValue[mutateSourceItemKey] = mutateSourceItem;
|
|
157
|
+
}
|
|
158
|
+
JSONPointer.set(mergedJson, sourceObjectPointer, sourceObjectValue);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return [JSON.stringify(mergedJson, null, 2)];
|
|
162
|
+
}
|
|
163
|
+
// helper function to replace locale placeholders in a string
|
|
164
|
+
// with the corresponding locale properties
|
|
165
|
+
// ex: {locale} -> will be replaced with the locale code
|
|
166
|
+
// ex: {localeName} -> will be replaced with the locale name
|
|
167
|
+
function replaceLocalePlaceholders(string, localeProperties) {
|
|
168
|
+
return string.replace(/\{(\w+)\}/g, (match, property) => {
|
|
169
|
+
// Handle common aliases
|
|
170
|
+
if (property === 'locale' || property === 'localeCode') {
|
|
171
|
+
return localeProperties.code;
|
|
172
|
+
}
|
|
173
|
+
if (property === 'localeName') {
|
|
174
|
+
return localeProperties.name;
|
|
175
|
+
}
|
|
176
|
+
if (property === 'localeNativeName') {
|
|
177
|
+
return localeProperties.nativeName;
|
|
178
|
+
}
|
|
179
|
+
// Check if the property exists in localeProperties
|
|
180
|
+
if (property in localeProperties) {
|
|
181
|
+
return localeProperties[property];
|
|
182
|
+
}
|
|
183
|
+
// Return the original placeholder if property not found
|
|
184
|
+
return match;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// apply transformations to the sourceItem in-place
|
|
188
|
+
export function applyTransformations(sourceItem, transform, targetLocale, defaultLocale) {
|
|
189
|
+
if (!transform)
|
|
190
|
+
return;
|
|
191
|
+
const targetLocaleProperties = getLocaleProperties(targetLocale);
|
|
192
|
+
const defaultLocaleProperties = getLocaleProperties(defaultLocale);
|
|
193
|
+
for (const [transformPath, transformOptions] of Object.entries(transform)) {
|
|
194
|
+
if (!transformOptions.replace ||
|
|
195
|
+
typeof transformOptions.replace !== 'string') {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const results = JSONPath({
|
|
199
|
+
json: sourceItem,
|
|
200
|
+
path: transformPath,
|
|
201
|
+
resultType: 'all',
|
|
202
|
+
flatten: true,
|
|
203
|
+
wrap: true,
|
|
204
|
+
});
|
|
205
|
+
if (!results || results.length === 0) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
results.forEach((result) => {
|
|
209
|
+
if (typeof result.value !== 'string') {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Replace locale placeholders in the replace string
|
|
213
|
+
let replaceString = transformOptions.replace;
|
|
214
|
+
// Replace all locale property placeholders
|
|
215
|
+
replaceString = replaceLocalePlaceholders(replaceString, targetLocaleProperties);
|
|
216
|
+
if (transformOptions.match &&
|
|
217
|
+
typeof transformOptions.match === 'string') {
|
|
218
|
+
// Replace locale placeholders in the match string using defaultLocale properties
|
|
219
|
+
let matchString = transformOptions.match;
|
|
220
|
+
matchString = replaceLocalePlaceholders(matchString, defaultLocaleProperties);
|
|
221
|
+
result.value = result.value.replace(new RegExp(matchString, 'g'), replaceString);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
result.value = replaceString;
|
|
225
|
+
}
|
|
226
|
+
// Update the actual sourceItem using JSONPointer
|
|
227
|
+
JSONPointer.set(sourceItem, result.pointer, result.value);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { flattenJsonWithStringFilter } from './flattenJson.js';
|
|
2
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
3
|
+
import { exit, logError } from '../../console/logging.js';
|
|
4
|
+
import { findMatchingItemArray, findMatchingItemObject, generateSourceObjectPointers, validateJsonSchema, } from './utils.js';
|
|
5
|
+
// Parse a JSON file according to a JSON schema
|
|
6
|
+
export function parseJson(content, filePath, options, defaultLocale) {
|
|
7
|
+
const jsonSchema = validateJsonSchema(options, filePath);
|
|
8
|
+
if (!jsonSchema) {
|
|
9
|
+
return content;
|
|
10
|
+
}
|
|
11
|
+
let json;
|
|
12
|
+
try {
|
|
13
|
+
json = JSON.parse(content);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
logError(`Invalid JSON file: ${filePath}`);
|
|
17
|
+
exit(1);
|
|
18
|
+
}
|
|
19
|
+
// Handle include
|
|
20
|
+
if (jsonSchema.include) {
|
|
21
|
+
const flattenedJson = flattenJsonWithStringFilter(json, jsonSchema.include);
|
|
22
|
+
return JSON.stringify(flattenedJson);
|
|
23
|
+
}
|
|
24
|
+
if (!jsonSchema.composite) {
|
|
25
|
+
logError('No composite property found in JSON schema');
|
|
26
|
+
exit(1);
|
|
27
|
+
}
|
|
28
|
+
// Construct lvl 1
|
|
29
|
+
// Create mapping of sourceObjectPointer to SourceObjectOptions
|
|
30
|
+
const sourceObjectPointers = generateSourceObjectPointers(jsonSchema.composite, json);
|
|
31
|
+
// Construct lvl 2
|
|
32
|
+
const sourceObjectsToTranslate = {};
|
|
33
|
+
for (const [sourceObjectPointer, { sourceObjectValue, sourceObjectOptions },] of Object.entries(sourceObjectPointers)) {
|
|
34
|
+
// Find the default locale in each source item in each sourceObjectValue
|
|
35
|
+
// Array: use key field
|
|
36
|
+
if (sourceObjectOptions.type === 'array') {
|
|
37
|
+
// Validate type
|
|
38
|
+
if (!Array.isArray(sourceObjectValue)) {
|
|
39
|
+
logError(`Source object value is not an array at path: ${sourceObjectPointer}`);
|
|
40
|
+
exit(1);
|
|
41
|
+
}
|
|
42
|
+
// Validate localeProperty
|
|
43
|
+
const matchingItem = findMatchingItemArray(defaultLocale, sourceObjectOptions, sourceObjectPointer, sourceObjectValue);
|
|
44
|
+
if (!matchingItem) {
|
|
45
|
+
logError(`Matching sourceItem not found at path: ${sourceObjectPointer} for locale: ${defaultLocale}. Please check your JSON schema`);
|
|
46
|
+
exit(1);
|
|
47
|
+
}
|
|
48
|
+
const { sourceItem, keyPointer } = matchingItem;
|
|
49
|
+
// Get the fields to translate from the includes
|
|
50
|
+
let itemsToTranslate = [];
|
|
51
|
+
for (const include of sourceObjectOptions.include) {
|
|
52
|
+
try {
|
|
53
|
+
const matchingItems = JSONPath({
|
|
54
|
+
json: sourceItem,
|
|
55
|
+
path: include,
|
|
56
|
+
resultType: 'all',
|
|
57
|
+
flatten: true,
|
|
58
|
+
wrap: true,
|
|
59
|
+
});
|
|
60
|
+
if (matchingItems) {
|
|
61
|
+
itemsToTranslate.push(...matchingItems);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (error) { }
|
|
65
|
+
}
|
|
66
|
+
itemsToTranslate = Object.fromEntries(itemsToTranslate
|
|
67
|
+
.filter((item) => item.pointer !== keyPointer)
|
|
68
|
+
.map((item) => [
|
|
69
|
+
item.pointer,
|
|
70
|
+
item.value,
|
|
71
|
+
]));
|
|
72
|
+
// Add the items to translate to the result
|
|
73
|
+
sourceObjectsToTranslate[sourceObjectPointer] = itemsToTranslate;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// Object: use the key in this object with the matching locale property
|
|
77
|
+
// Validate type
|
|
78
|
+
if (typeof sourceObjectValue !== 'object' || sourceObjectValue === null) {
|
|
79
|
+
logError(`Source object value is not an object at path: ${sourceObjectPointer}`);
|
|
80
|
+
exit(1);
|
|
81
|
+
}
|
|
82
|
+
// Validate localeProperty
|
|
83
|
+
const matchingItem = findMatchingItemObject(defaultLocale, sourceObjectPointer, sourceObjectOptions, sourceObjectValue);
|
|
84
|
+
// Validate source item exists
|
|
85
|
+
if (!matchingItem.sourceItem) {
|
|
86
|
+
logError(`Source item not found at path: ${sourceObjectPointer}. You must specify a source item where its key matches the default locale`);
|
|
87
|
+
exit(1);
|
|
88
|
+
}
|
|
89
|
+
const { sourceItem } = matchingItem;
|
|
90
|
+
// Get the fields to translate from the includes
|
|
91
|
+
const itemsToTranslate = flattenJsonWithStringFilter(sourceItem, sourceObjectOptions.include);
|
|
92
|
+
// Add the items to translate to the result
|
|
93
|
+
sourceObjectsToTranslate[sourceObjectPointer] = itemsToTranslate;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return JSON.stringify(sourceObjectsToTranslate);
|
|
97
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AdditionalOptions, JsonSchema, SourceObjectOptions } from '../../types/index.js';
|
|
2
|
+
export declare function findMatchingItemArray(locale: string, sourceObjectOptions: SourceObjectOptions, sourceObjectPointer: string, sourceObjectValue: any): {
|
|
3
|
+
sourceItem: any;
|
|
4
|
+
keyParentProperty: string;
|
|
5
|
+
itemIndex: number;
|
|
6
|
+
keyPointer: string;
|
|
7
|
+
} | null;
|
|
8
|
+
export declare function findMatchingItemObject(locale: string, sourceObjectPointer: string, sourceObjectOptions: SourceObjectOptions, sourceObjectValue: any): {
|
|
9
|
+
sourceItem: any | undefined;
|
|
10
|
+
keyParentProperty: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function getIdentifyingLocaleProperty(locale: string, sourceObjectPointer: string, sourceObjectOptions: SourceObjectOptions): string;
|
|
13
|
+
export declare function getSourceObjectOptionsArray(locale: string, sourceObjectPointer: string, sourceObjectOptions: SourceObjectOptions): {
|
|
14
|
+
identifyingLocaleProperty: string;
|
|
15
|
+
localeKeyJsonPath: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function getSourceObjectOptionsObject(defaultLocale: string, sourceObjectPointer: string, sourceObjectOptions: SourceObjectOptions): {
|
|
18
|
+
identifyingLocaleProperty: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function generateSourceObjectPointers(jsonSchema: {
|
|
21
|
+
[sourceObjectPath: string]: SourceObjectOptions;
|
|
22
|
+
}, originalJson: any): Record<string, {
|
|
23
|
+
sourceObjectValue: any;
|
|
24
|
+
sourceObjectOptions: SourceObjectOptions;
|
|
25
|
+
}>;
|
|
26
|
+
export declare function validateJsonSchema(options: AdditionalOptions, filePath: string): JsonSchema | null;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { getLocaleProperties } from 'generaltranslation';
|
|
2
|
+
import { exit, logError } from '../../console/logging.js';
|
|
3
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
4
|
+
import { flattenJson } from './flattenJson.js';
|
|
5
|
+
import micromatch from 'micromatch';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
const { isMatch } = micromatch;
|
|
8
|
+
// Find the matching source item in an array
|
|
9
|
+
// where the key matches the identifying locale property
|
|
10
|
+
// If no matching item is found, exit with an error
|
|
11
|
+
export function findMatchingItemArray(locale, sourceObjectOptions, sourceObjectPointer, sourceObjectValue) {
|
|
12
|
+
const { identifyingLocaleProperty, localeKeyJsonPath } = getSourceObjectOptionsArray(locale, sourceObjectPointer, sourceObjectOptions);
|
|
13
|
+
// Use the json pointer key to locate the source item
|
|
14
|
+
for (const [index, item] of sourceObjectValue.entries()) {
|
|
15
|
+
// Get the key candidates
|
|
16
|
+
const keyCandidates = JSONPath({
|
|
17
|
+
json: item,
|
|
18
|
+
path: localeKeyJsonPath,
|
|
19
|
+
resultType: 'all',
|
|
20
|
+
flatten: true,
|
|
21
|
+
wrap: true,
|
|
22
|
+
});
|
|
23
|
+
if (!keyCandidates) {
|
|
24
|
+
logError(`Source item at path: ${sourceObjectPointer} does not have a key value at path: ${localeKeyJsonPath}`);
|
|
25
|
+
exit(1);
|
|
26
|
+
}
|
|
27
|
+
else if (keyCandidates.length !== 1) {
|
|
28
|
+
logError(`Source item at path: ${sourceObjectPointer} has multiple matching keys with path: ${localeKeyJsonPath}`);
|
|
29
|
+
exit(1);
|
|
30
|
+
}
|
|
31
|
+
// Validate the key is the identifying locale property
|
|
32
|
+
if (!keyCandidates[0] ||
|
|
33
|
+
identifyingLocaleProperty !== keyCandidates[0].value) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
sourceItem: item,
|
|
38
|
+
keyParentProperty: keyCandidates[0].parentProperty,
|
|
39
|
+
itemIndex: index,
|
|
40
|
+
keyPointer: keyCandidates[0].pointer,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
export function findMatchingItemObject(locale, sourceObjectPointer, sourceObjectOptions, sourceObjectValue) {
|
|
46
|
+
const { identifyingLocaleProperty } = getSourceObjectOptionsObject(locale, sourceObjectPointer, sourceObjectOptions);
|
|
47
|
+
// Locate the source item
|
|
48
|
+
if (sourceObjectValue[identifyingLocaleProperty]) {
|
|
49
|
+
return {
|
|
50
|
+
sourceItem: sourceObjectValue[identifyingLocaleProperty],
|
|
51
|
+
keyParentProperty: identifyingLocaleProperty,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
sourceItem: undefined,
|
|
56
|
+
keyParentProperty: identifyingLocaleProperty,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function getIdentifyingLocaleProperty(locale, sourceObjectPointer, sourceObjectOptions) {
|
|
60
|
+
// Validate localeProperty
|
|
61
|
+
const localeProperty = sourceObjectOptions.localeProperty || 'code';
|
|
62
|
+
const identifyingLocaleProperty = getLocaleProperties(locale)[localeProperty];
|
|
63
|
+
if (!identifyingLocaleProperty) {
|
|
64
|
+
logError(`Source object options localeProperty is not a valid locale property at path: ${sourceObjectPointer}`);
|
|
65
|
+
exit(1);
|
|
66
|
+
}
|
|
67
|
+
return identifyingLocaleProperty;
|
|
68
|
+
}
|
|
69
|
+
export function getSourceObjectOptionsArray(locale, sourceObjectPointer, sourceObjectOptions) {
|
|
70
|
+
const identifyingLocaleProperty = getIdentifyingLocaleProperty(locale, sourceObjectPointer, sourceObjectOptions);
|
|
71
|
+
const localeKeyJsonPath = sourceObjectOptions.key;
|
|
72
|
+
if (!localeKeyJsonPath) {
|
|
73
|
+
logError(`Source object options key is required for array at path: ${sourceObjectPointer}`);
|
|
74
|
+
exit(1);
|
|
75
|
+
}
|
|
76
|
+
return { identifyingLocaleProperty, localeKeyJsonPath };
|
|
77
|
+
}
|
|
78
|
+
export function getSourceObjectOptionsObject(defaultLocale, sourceObjectPointer, sourceObjectOptions) {
|
|
79
|
+
const identifyingLocaleProperty = getIdentifyingLocaleProperty(defaultLocale, sourceObjectPointer, sourceObjectOptions);
|
|
80
|
+
const jsonPathKey = sourceObjectOptions.key;
|
|
81
|
+
if (jsonPathKey) {
|
|
82
|
+
logError(`Source object options key is not allowed for object at path: ${sourceObjectPointer}`);
|
|
83
|
+
exit(1);
|
|
84
|
+
}
|
|
85
|
+
return { identifyingLocaleProperty };
|
|
86
|
+
}
|
|
87
|
+
// Generate a mapping of sourceObjectPointer to SourceObjectOptions
|
|
88
|
+
// where the sourceObjectPointer is a jsonpointer to the array or object containing
|
|
89
|
+
export function generateSourceObjectPointers(jsonSchema, originalJson) {
|
|
90
|
+
const sourceObjectPointers = Object.entries(jsonSchema).reduce((acc, [sourceObjectPath, sourceObjectOptions]) => {
|
|
91
|
+
const sourceObjects = flattenJson(originalJson, [sourceObjectPath]);
|
|
92
|
+
Object.entries(sourceObjects).forEach(([pointer, value]) => {
|
|
93
|
+
acc[pointer] = {
|
|
94
|
+
sourceObjectValue: value,
|
|
95
|
+
sourceObjectOptions,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
return acc;
|
|
99
|
+
}, {});
|
|
100
|
+
return sourceObjectPointers;
|
|
101
|
+
}
|
|
102
|
+
export function validateJsonSchema(options, filePath) {
|
|
103
|
+
if (!options.jsonSchema) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const fileGlobs = Object.keys(options.jsonSchema);
|
|
107
|
+
const matchingGlob = fileGlobs.find((fileGlob) => isMatch(path.relative(process.cwd(), filePath), fileGlob));
|
|
108
|
+
if (!matchingGlob || !options.jsonSchema[matchingGlob]) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
// Validate includes or composite
|
|
112
|
+
const jsonSchema = options.jsonSchema[matchingGlob];
|
|
113
|
+
if (jsonSchema.include && jsonSchema.composite) {
|
|
114
|
+
logError('include and composite cannot be used together in the same JSON schema');
|
|
115
|
+
exit(1);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (!jsonSchema.include && !jsonSchema.composite) {
|
|
119
|
+
logError('No include or composite property found in JSON schema');
|
|
120
|
+
exit(1);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return jsonSchema;
|
|
124
|
+
}
|
|
@@ -35,7 +35,7 @@ export function resolveFiles(files, locale, cwd) {
|
|
|
35
35
|
const transformPaths = {};
|
|
36
36
|
// Process GT files
|
|
37
37
|
if (files.gt?.output) {
|
|
38
|
-
placeholderResult.gt = files.gt.output;
|
|
38
|
+
placeholderResult.gt = path.resolve(cwd, files.gt.output);
|
|
39
39
|
}
|
|
40
40
|
for (const fileType of SUPPORTED_FILE_EXTENSIONS) {
|
|
41
41
|
// ==== TRANSFORMS ==== //
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { displayCreatedConfigFile } from '../../console/logging.js';
|
|
3
3
|
import { logError } from '../../console/logging.js';
|
|
4
|
+
import { GT_CONFIG_SCHEMA_URL } from '../../utils/constants.js';
|
|
4
5
|
/**
|
|
5
6
|
* Checks if the config file exists.
|
|
6
7
|
* If yes, make sure make sure projectId is correct
|
|
@@ -25,6 +26,7 @@ export async function createOrUpdateConfig(configFilepath, options) {
|
|
|
25
26
|
}
|
|
26
27
|
// merge old and new content
|
|
27
28
|
const mergedContent = {
|
|
29
|
+
$schema: GT_CONFIG_SCHEMA_URL,
|
|
28
30
|
...oldContent,
|
|
29
31
|
...newContent,
|
|
30
32
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/data.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type JSONDictionary = {
|
|
|
17
17
|
export type FlattenedJSONDictionary = {
|
|
18
18
|
[key: string]: string;
|
|
19
19
|
};
|
|
20
|
-
export type { FileFormat, DataFormat } from 'generaltranslation/types';
|
|
20
|
+
export type { FileFormat, DataFormat, FileToTranslate, } from 'generaltranslation/types';
|
|
21
21
|
export type JsxChildren = string | string[] | any;
|
|
22
22
|
export type Translations = {
|
|
23
23
|
[key: string]: JsxChildren;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -92,4 +92,30 @@ export type Settings = {
|
|
|
92
92
|
description?: string;
|
|
93
93
|
src: string[];
|
|
94
94
|
framework?: SupportedFrameworks;
|
|
95
|
+
options?: AdditionalOptions;
|
|
96
|
+
};
|
|
97
|
+
export type AdditionalOptions = {
|
|
98
|
+
jsonSchema?: {
|
|
99
|
+
[fileGlob: string]: JsonSchema;
|
|
100
|
+
};
|
|
101
|
+
docsUrlPattern?: string;
|
|
102
|
+
};
|
|
103
|
+
export type JsonSchema = {
|
|
104
|
+
preset?: 'mintlify';
|
|
105
|
+
include?: string[];
|
|
106
|
+
composite?: {
|
|
107
|
+
[sourceObjectPath: string]: SourceObjectOptions;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
export type SourceObjectOptions = {
|
|
111
|
+
type: 'array' | 'object';
|
|
112
|
+
include: string[];
|
|
113
|
+
key?: string;
|
|
114
|
+
localeProperty?: string;
|
|
115
|
+
transform?: {
|
|
116
|
+
[transformPath: string]: {
|
|
117
|
+
match?: string;
|
|
118
|
+
replace: string;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
95
121
|
};
|
package/dist/utils/constants.js
CHANGED
|
@@ -30,23 +30,28 @@ export default async function localizeStaticUrls(settings) {
|
|
|
30
30
|
// Get file content
|
|
31
31
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
32
32
|
// Localize the file
|
|
33
|
-
const localizedFile = localizeStaticUrlsForFile(fileContent, settings.defaultLocale, locale, settings.experimentalHideDefaultLocale || false);
|
|
33
|
+
const localizedFile = localizeStaticUrlsForFile(fileContent, settings.defaultLocale, locale, settings.experimentalHideDefaultLocale || false, settings.options?.docsUrlPattern);
|
|
34
34
|
// Write the localized file to the target path
|
|
35
35
|
await fs.promises.writeFile(filePath, localizedFile);
|
|
36
36
|
}));
|
|
37
37
|
}));
|
|
38
38
|
}
|
|
39
|
-
//
|
|
40
|
-
function localizeStaticUrlsForFile(file, defaultLocale, targetLocale, hideDefaultLocale
|
|
39
|
+
// Naive find and replace, in the future, construct an AST
|
|
40
|
+
function localizeStaticUrlsForFile(file, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]' // eg /docs/[locale] or /[locale]
|
|
41
|
+
) {
|
|
42
|
+
if (!pattern.startsWith('/')) {
|
|
43
|
+
pattern = '/' + pattern;
|
|
44
|
+
}
|
|
41
45
|
// 1. Search for all instances of:
|
|
46
|
+
const patternHead = pattern.split('[locale]')[0];
|
|
42
47
|
let regex;
|
|
43
48
|
if (hideDefaultLocale) {
|
|
44
49
|
// Match complete markdown links: `](/docs/...)` or `](/docs)`
|
|
45
|
-
regex = new RegExp(`\\]\\(
|
|
50
|
+
regex = new RegExp(`\\]\\(${patternHead}(?:/([^)]*))?\\)`, 'g');
|
|
46
51
|
}
|
|
47
52
|
else {
|
|
48
53
|
// Match complete markdown links with default locale: `](/docs/${defaultLocale}/...)` or `](/docs/${defaultLocale})`
|
|
49
|
-
regex = new RegExp(`\\]\\(
|
|
54
|
+
regex = new RegExp(`\\]\\(${patternHead}${defaultLocale}(?:/([^)]*))?\\)`, 'g');
|
|
50
55
|
}
|
|
51
56
|
const matches = file.match(regex);
|
|
52
57
|
if (!matches) {
|
|
@@ -64,14 +69,14 @@ function localizeStaticUrlsForFile(file, defaultLocale, targetLocale, hideDefaul
|
|
|
64
69
|
}
|
|
65
70
|
// Add target locale to the path
|
|
66
71
|
if (!pathContent || pathContent === '') {
|
|
67
|
-
return `](
|
|
72
|
+
return `](${patternHead}${targetLocale})`;
|
|
68
73
|
}
|
|
69
|
-
return `](
|
|
74
|
+
return `](${patternHead}${targetLocale}/${pathContent})`;
|
|
70
75
|
}
|
|
71
76
|
else {
|
|
72
77
|
// For non-hideDefaultLocale, replace defaultLocale with targetLocale
|
|
73
78
|
// pathContent contains everything after the default locale (no leading slash if present)
|
|
74
|
-
return `](
|
|
79
|
+
return `](${patternHead}${targetLocale}${pathContent ? '/' + pathContent : ''})`;
|
|
75
80
|
}
|
|
76
81
|
});
|
|
77
82
|
return localizedFile;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gtx-cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": "dist/main.js",
|
|
6
6
|
"files": [
|
|
@@ -87,7 +87,10 @@
|
|
|
87
87
|
"esbuild": "^0.25.4",
|
|
88
88
|
"fast-glob": "^3.3.3",
|
|
89
89
|
"form-data": "^4.0.2",
|
|
90
|
-
"generaltranslation": "^7.1.
|
|
90
|
+
"generaltranslation": "^7.1.4",
|
|
91
|
+
"jsonpath-plus": "^10.3.0",
|
|
92
|
+
"jsonpointer": "^5.0.1",
|
|
93
|
+
"micromatch": "^4.0.8",
|
|
91
94
|
"open": "^10.1.1",
|
|
92
95
|
"ora": "^8.2.0",
|
|
93
96
|
"resolve": "^1.22.10",
|