gtx-cli 2.0.7 → 2.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.0.8
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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.
8
+
9
+ - Updated dependencies [[`a7eca74`](https://github.com/generaltranslation/gt/commit/a7eca74677356b392c7c1a431f664c8e28adbf0c)]:
10
+ - generaltranslation@7.1.4
11
+
3
12
  ## 2.0.7
4
13
 
5
14
  ### 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(projectId: string, apiKey: string, baseUrl: string, data: {
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(projectId, apiKey, baseUrl, data, locales, timeoutDuration, resolveOutputPath, downloadStatus) {
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(baseUrl, projectId, apiKey, fileQueryData, downloadStatus, spinner, resolveOutputPath);
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(baseUrl, projectId, apiKey, fileQueryData, downloadStatus, spinner, resolveOutputPath);
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(baseUrl, projectId, apiKey, fileQueryData, downloadStatus, spinner, resolveOutputPath) {
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>;
@@ -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, Buffer.from(fileData));
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, file.data);
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}',
@@ -91,6 +92,18 @@ export async function generateSettings(options, cwd = process.cwd()) {
91
92
  mergedOptions.files = mergedOptions.files
92
93
  ? resolveFiles(mergedOptions.files, mergedOptions.defaultLocale, cwd)
93
94
  : undefined;
95
+ // Add additional options if provided
96
+ if (mergedOptions.options && mergedOptions.options.jsonSchema) {
97
+ for (const fileGlob of Object.keys(mergedOptions.options.jsonSchema)) {
98
+ const jsonSchema = mergedOptions.options.jsonSchema[fileGlob];
99
+ if (jsonSchema.preset) {
100
+ mergedOptions.options.jsonSchema[fileGlob] = {
101
+ ...generatePreset(jsonSchema.preset),
102
+ ...jsonSchema,
103
+ };
104
+ }
105
+ }
106
+ }
94
107
  // if there's no existing config file, creates one
95
108
  // does not include the API key to avoid exposing it
96
109
  if (!fs.existsSync(mergedOptions.config)) {
@@ -0,0 +1,2 @@
1
+ import { JsonSchema } from '../types/index.js';
2
+ export declare function generatePreset(preset: string): JsonSchema;
@@ -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
+ }
@@ -1,3 +1,3 @@
1
1
  import { Settings } from '../types/index.js';
2
2
  export declare function validateSettings(settings: Settings): void;
3
- export declare function validateConfigExists(): string | undefined;
3
+ export declare function validateConfigExists(): string;
@@ -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): void;
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;
@@ -22,7 +22,10 @@ export function logMessage(message) {
22
22
  }
23
23
  export function logErrorAndExit(message) {
24
24
  log.error(message);
25
- process.exit(1);
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 json = JSON.parse(content);
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(options.projectId, options.apiKey, options.baseUrl, data, locales, 600, (sourcePath, locale) => fileMapping[locale][sourcePath], downloadStatus // Pass the already downloaded files to avoid duplicate requests
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,2 @@
1
+ import { AdditionalOptions } from '../../types/index.js';
2
+ export declare function parseJson(content: string, filePath: string, options: AdditionalOptions, defaultLocale: string): string;
@@ -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,6 @@
1
+ type JSONValue = string | number | boolean | null | JSONObject | JSONArray;
2
+ export type JSONObject = {
3
+ [key: string]: JSONValue;
4
+ };
5
+ export type JSONArray = JSONValue[];
6
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -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;
@@ -92,4 +92,29 @@ 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
+ };
102
+ export type JsonSchema = {
103
+ preset?: 'mintlify';
104
+ include?: string[];
105
+ composite?: {
106
+ [sourceObjectPath: string]: SourceObjectOptions;
107
+ };
108
+ };
109
+ export type SourceObjectOptions = {
110
+ type: 'array' | 'object';
111
+ include: string[];
112
+ key?: string;
113
+ localeProperty?: string;
114
+ transform?: {
115
+ [transformPath: string]: {
116
+ match?: string;
117
+ replace: string;
118
+ };
119
+ };
95
120
  };
@@ -1 +1,2 @@
1
1
  export declare const GT_DASHBOARD_URL = "https://dash.generaltranslation.com";
2
+ export declare const GT_CONFIG_SCHEMA_URL = "https://assets.gtx.dev/config-schema.json";
@@ -1 +1,2 @@
1
1
  export const GT_DASHBOARD_URL = 'https://dash.generaltranslation.com';
2
+ export const GT_CONFIG_SCHEMA_URL = 'https://assets.gtx.dev/config-schema.json';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtx-cli",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
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.2",
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",