gtx-cli 1.2.30-alpha.32 → 1.2.30-alpha.34

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/api/checkFileTranslations.d.ts +1 -1
  3. package/dist/api/checkFileTranslations.js +8 -7
  4. package/dist/api/downloadFile.d.ts +1 -1
  5. package/dist/api/downloadFile.js +3 -2
  6. package/dist/api/downloadFileBatch.d.ts +1 -1
  7. package/dist/api/downloadFileBatch.js +3 -2
  8. package/dist/api/fetchTranslations.d.ts +1 -1
  9. package/dist/api/fetchTranslations.js +3 -2
  10. package/dist/api/sendFiles.js +2 -2
  11. package/dist/api/sendUpdates.js +2 -1
  12. package/dist/api/waitForUpdates.d.ts +1 -1
  13. package/dist/api/waitForUpdates.js +3 -2
  14. package/dist/cli/react.js +14 -1
  15. package/dist/config/generateSettings.js +24 -12
  16. package/dist/config/resolveConfig.d.ts +4 -0
  17. package/dist/config/resolveConfig.js +19 -0
  18. package/dist/formats/files/translate.d.ts +4 -0
  19. package/dist/formats/files/translate.js +7 -5
  20. package/dist/fs/config/loadConfig.d.ts +1 -1
  21. package/dist/fs/config/loadConfig.js +1 -1
  22. package/dist/fs/config/parseFilesConfig.js +13 -12
  23. package/dist/fs/config/setupConfig.d.ts +1 -0
  24. package/dist/fs/config/setupConfig.js +1 -0
  25. package/dist/translation/translate.js +2 -2
  26. package/dist/types/index.d.ts +3 -0
  27. package/dist/utils/flattenJsonFiles.d.ts +2 -0
  28. package/dist/utils/flattenJsonFiles.js +36 -0
  29. package/dist/utils/headers.d.ts +1 -0
  30. package/dist/utils/headers.js +14 -0
  31. package/dist/utils/localizeStaticUrls.d.ts +15 -0
  32. package/dist/utils/localizeStaticUrls.js +78 -0
  33. package/dist/utils/sanitizeFileContent.d.ts +6 -0
  34. package/dist/utils/sanitizeFileContent.js +29 -0
  35. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 1.2.34
4
+
5
+ ### Patch Changes
6
+
7
+ - [#428](https://github.com/generaltranslation/gt/pull/428) [`54036f5`](https://github.com/generaltranslation/gt/commit/54036f54308bdb9f9e6dcec93871e004dcf1be4c) Thanks [@ErnestM1234](https://github.com/ErnestM1234)! - feat: add experimental options to translate
8
+
9
+ ## 1.2.33
10
+
11
+ ### Patch Changes
12
+
13
+ - [#426](https://github.com/generaltranslation/gt/pull/426) [`ce57545`](https://github.com/generaltranslation/gt/commit/ce575454301185c663cfb93345d3058c9ceb25dd) Thanks [@brian-lou](https://github.com/brian-lou)! - Improve file pattern matching
14
+
15
+ ## 1.2.31
16
+
17
+ ### Patch Changes
18
+
19
+ - [#423](https://github.com/generaltranslation/gt/pull/423) [`0ed08c7`](https://github.com/generaltranslation/gt/commit/0ed08c7bb1e63c99296b74138e4d44b718681fc8) Thanks [@brian-lou](https://github.com/brian-lou)! - Add setting configuration options
20
+
3
21
  ## 1.2.30
4
22
 
5
23
  ### Patch Changes
@@ -8,7 +8,7 @@
8
8
  * @param timeoutDuration - The timeout duration for the wait in seconds
9
9
  * @returns True if all translations are deployed, false otherwise
10
10
  */
11
- export declare function checkFileTranslations(apiKey: string, baseUrl: string, data: {
11
+ export declare function checkFileTranslations(projectId: string, apiKey: string, baseUrl: string, data: {
12
12
  [key: string]: {
13
13
  versionId: string;
14
14
  fileName: string;
@@ -3,6 +3,7 @@ import { createOraSpinner, logError } from '../console/logging.js';
3
3
  import { getLocaleProperties } from 'generaltranslation';
4
4
  import { downloadFile } from './downloadFile.js';
5
5
  import { downloadFileBatch } from './downloadFileBatch.js';
6
+ import { getAuthHeaders } from '../utils/headers.js';
6
7
  /**
7
8
  * Checks the status of translations for a given version ID
8
9
  * @param apiKey - The API key for the General Translation API
@@ -13,7 +14,7 @@ import { downloadFileBatch } from './downloadFileBatch.js';
13
14
  * @param timeoutDuration - The timeout duration for the wait in seconds
14
15
  * @returns True if all translations are deployed, false otherwise
15
16
  */
16
- export async function checkFileTranslations(apiKey, baseUrl, data, locales, timeoutDuration, resolveOutputPath, downloadStatus) {
17
+ export async function checkFileTranslations(projectId, apiKey, baseUrl, data, locales, timeoutDuration, resolveOutputPath, downloadStatus) {
17
18
  const startTime = Date.now();
18
19
  console.log();
19
20
  const spinner = await createOraSpinner();
@@ -21,7 +22,7 @@ export async function checkFileTranslations(apiKey, baseUrl, data, locales, time
21
22
  // Initialize the query data
22
23
  const fileQueryData = prepareFileQueryData(data, locales);
23
24
  // Do first check immediately
24
- const initialCheck = await checkTranslationDeployment(baseUrl, apiKey, fileQueryData, downloadStatus, spinner, resolveOutputPath);
25
+ const initialCheck = await checkTranslationDeployment(baseUrl, projectId, apiKey, fileQueryData, downloadStatus, spinner, resolveOutputPath);
25
26
  if (initialCheck) {
26
27
  spinner.succeed(chalk.green('Files translated!'));
27
28
  return true;
@@ -33,7 +34,7 @@ export async function checkFileTranslations(apiKey, baseUrl, data, locales, time
33
34
  // Start the interval aligned with the original request time
34
35
  setTimeout(() => {
35
36
  intervalCheck = setInterval(async () => {
36
- const isDeployed = await checkTranslationDeployment(baseUrl, apiKey, fileQueryData, downloadStatus, spinner, resolveOutputPath);
37
+ const isDeployed = await checkTranslationDeployment(baseUrl, projectId, apiKey, fileQueryData, downloadStatus, spinner, resolveOutputPath);
37
38
  const elapsed = Date.now() - startTime;
38
39
  if (isDeployed || elapsed >= timeoutDuration * 1000) {
39
40
  clearInterval(intervalCheck);
@@ -156,7 +157,7 @@ function generateStatusSuffixText(downloadStatus, fileQueryData) {
156
157
  /**
157
158
  * Checks translation status and downloads ready files
158
159
  */
159
- async function checkTranslationDeployment(baseUrl, apiKey, fileQueryData, downloadStatus, spinner, resolveOutputPath) {
160
+ async function checkTranslationDeployment(baseUrl, projectId, apiKey, fileQueryData, downloadStatus, spinner, resolveOutputPath) {
160
161
  try {
161
162
  // Only query for files that haven't been downloaded yet
162
163
  const currentQueryData = fileQueryData.filter((item) => !downloadStatus.downloaded.has(`${item.fileName}:${item.locale}`) &&
@@ -169,7 +170,7 @@ async function checkTranslationDeployment(baseUrl, apiKey, fileQueryData, downlo
169
170
  method: 'POST',
170
171
  headers: {
171
172
  'Content-Type': 'application/json',
172
- ...(apiKey && { 'x-gt-api-key': apiKey }),
173
+ ...getAuthHeaders(projectId, apiKey),
173
174
  },
174
175
  body: JSON.stringify({ files: currentQueryData }),
175
176
  });
@@ -193,7 +194,7 @@ async function checkTranslationDeployment(baseUrl, apiKey, fileQueryData, downlo
193
194
  });
194
195
  // Use batch download if there are multiple files
195
196
  if (batchFiles.length > 1) {
196
- const batchResult = await downloadFileBatch(baseUrl, apiKey, batchFiles.map(({ translationId, outputPath }) => ({
197
+ const batchResult = await downloadFileBatch(baseUrl, projectId, apiKey, batchFiles.map(({ translationId, outputPath }) => ({
197
198
  translationId,
198
199
  outputPath,
199
200
  })));
@@ -211,7 +212,7 @@ async function checkTranslationDeployment(baseUrl, apiKey, fileQueryData, downlo
211
212
  else if (batchFiles.length === 1) {
212
213
  // For a single file, use the original downloadFile method
213
214
  const file = batchFiles[0];
214
- const result = await downloadFile(baseUrl, apiKey, file.translationId, file.outputPath);
215
+ const result = await downloadFile(baseUrl, projectId, apiKey, file.translationId, file.outputPath);
215
216
  if (result) {
216
217
  downloadStatus.downloaded.add(file.fileLocale);
217
218
  }
@@ -1 +1 @@
1
- export declare function downloadFile(baseUrl: string, apiKey: string, translationId: string, outputPath: string, maxRetries?: number, retryDelay?: number): Promise<boolean>;
1
+ export declare function downloadFile(baseUrl: string, projectId: string, apiKey: string, translationId: string, outputPath: string, maxRetries?: number, retryDelay?: number): Promise<boolean>;
@@ -1,15 +1,16 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { logError } from '../console/logging.js';
4
+ import { getAuthHeaders } from '../utils/headers.js';
4
5
  // Helper function to download a file
5
- export async function downloadFile(baseUrl, apiKey, translationId, outputPath, maxRetries = 3, retryDelay = 1000) {
6
+ export async function downloadFile(baseUrl, projectId, apiKey, translationId, outputPath, maxRetries = 3, retryDelay = 1000) {
6
7
  let retries = 0;
7
8
  while (retries <= maxRetries) {
8
9
  try {
9
10
  const downloadResponse = await fetch(`${baseUrl}/v1/project/translations/files/${translationId}/download`, {
10
11
  method: 'GET',
11
12
  headers: {
12
- ...(apiKey && { 'x-gt-api-key': apiKey }),
13
+ ...getAuthHeaders(projectId, apiKey),
13
14
  },
14
15
  });
15
16
  if (downloadResponse.ok) {
@@ -7,7 +7,7 @@
7
7
  * @param retryDelay - Delay between retries in milliseconds
8
8
  * @returns Object containing successful and failed file IDs
9
9
  */
10
- export declare function downloadFileBatch(baseUrl: string, apiKey: string, files: Array<{
10
+ export declare function downloadFileBatch(baseUrl: string, projectId: string, apiKey: string, files: Array<{
11
11
  translationId: string;
12
12
  outputPath: string;
13
13
  }>, maxRetries?: number, retryDelay?: number): Promise<{
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { logError, logWarning } from '../console/logging.js';
4
+ import { getAuthHeaders } from '../utils/headers.js';
4
5
  /**
5
6
  * Downloads multiple translation files in a single batch request
6
7
  * @param baseUrl - The base URL for the General Translation API
@@ -10,7 +11,7 @@ import { logError, logWarning } from '../console/logging.js';
10
11
  * @param retryDelay - Delay between retries in milliseconds
11
12
  * @returns Object containing successful and failed file IDs
12
13
  */
13
- export async function downloadFileBatch(baseUrl, apiKey, files, maxRetries = 3, retryDelay = 1000) {
14
+ export async function downloadFileBatch(baseUrl, projectId, apiKey, files, maxRetries = 3, retryDelay = 1000) {
14
15
  let retries = 0;
15
16
  const fileIds = files.map((file) => file.translationId);
16
17
  const result = { successful: [], failed: [] };
@@ -22,7 +23,7 @@ export async function downloadFileBatch(baseUrl, apiKey, files, maxRetries = 3,
22
23
  method: 'POST',
23
24
  headers: {
24
25
  'Content-Type': 'application/json',
25
- ...(apiKey && { 'x-gt-api-key': apiKey }),
26
+ ...getAuthHeaders(projectId, apiKey),
26
27
  },
27
28
  body: JSON.stringify({ fileIds }),
28
29
  });
@@ -7,4 +7,4 @@ import { RetrievedTranslations } from '../types/api.js';
7
7
  * @param translationsDir - The directory to save the translations to
8
8
  * @param fileType - The file type to save the translations as (file extension)
9
9
  */
10
- export declare function fetchTranslations(baseUrl: string, apiKey: string, versionId: string): Promise<RetrievedTranslations>;
10
+ export declare function fetchTranslations(baseUrl: string, projectId: string, apiKey: string, versionId: string): Promise<RetrievedTranslations>;
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { logError } from '../console/logging.js';
3
+ import { getAuthHeaders } from '../utils/headers.js';
3
4
  /**
4
5
  * Fetches translations from the API and saves them to a local directory
5
6
  * @param baseUrl - The base URL for the API
@@ -8,13 +9,13 @@ import { logError } from '../console/logging.js';
8
9
  * @param translationsDir - The directory to save the translations to
9
10
  * @param fileType - The file type to save the translations as (file extension)
10
11
  */
11
- export async function fetchTranslations(baseUrl, apiKey, versionId) {
12
+ export async function fetchTranslations(baseUrl, projectId, apiKey, versionId) {
12
13
  // First fetch the translations from the API
13
14
  const response = await fetch(`${baseUrl}/v1/project/translations/info/${encodeURIComponent(versionId)}`, {
14
15
  method: 'GET',
15
16
  headers: {
16
17
  'Content-Type': 'application/json',
17
- ...(apiKey && { 'x-gt-api-key': apiKey }),
18
+ ...getAuthHeaders(projectId, apiKey),
18
19
  },
19
20
  });
20
21
  if (response.ok) {
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { createSpinner, logMessage, logSuccess } from '../console/logging.js';
3
+ import { getAuthHeaders } from '../utils/headers.js';
3
4
  /**
4
5
  * Sends multiple files for translation to the API
5
6
  * @param files - Array of file objects to translate
@@ -7,7 +8,6 @@ import { createSpinner, logMessage, logSuccess } from '../console/logging.js';
7
8
  * @returns The translated content or version ID
8
9
  */
9
10
  export async function sendFiles(files, options) {
10
- const { apiKey } = options;
11
11
  logMessage(chalk.cyan('Files to translate:') +
12
12
  '\n' +
13
13
  files.map((file) => ` - ${chalk.bold(file.fileName)}`).join('\n'));
@@ -35,7 +35,7 @@ export async function sendFiles(files, options) {
35
35
  const response = await fetch(`${options.baseUrl}/v1/project/translations/files/upload`, {
36
36
  method: 'POST',
37
37
  headers: {
38
- ...(apiKey && { 'x-gt-api-key': apiKey }),
38
+ ...getAuthHeaders(options.projectId, options.apiKey),
39
39
  },
40
40
  body: formData,
41
41
  });
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import { createSpinner, logSuccess, logWarning } from '../console/logging.js';
3
3
  import updateConfig from '../fs/config/updateConfig.js';
4
4
  import { isUsingLocalTranslations } from '../config/utils.js';
5
+ import { getAuthHeaders } from '../utils/headers.js';
5
6
  /**
6
7
  * Sends updates to the API
7
8
  * @param updates - The updates to send
@@ -35,7 +36,7 @@ export async function sendUpdates(updates, options, library) {
35
36
  method: 'POST',
36
37
  headers: {
37
38
  'Content-Type': 'application/json',
38
- ...(apiKey && { 'x-gt-api-key': apiKey }),
39
+ ...getAuthHeaders(options.projectId, options.apiKey),
39
40
  },
40
41
  body: JSON.stringify(body),
41
42
  });
@@ -8,4 +8,4 @@
8
8
  * @param timeoutDuration - The timeout duration for the wait
9
9
  * @returns True if all translations are deployed, false otherwise
10
10
  */
11
- export declare const waitForUpdates: (apiKey: string, baseUrl: string, versionId: string, startTime: number, timeoutDuration: number) => Promise<boolean>;
11
+ export declare const waitForUpdates: (projectId: string, apiKey: string, baseUrl: string, versionId: string, startTime: number, timeoutDuration: number) => Promise<boolean>;
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { createOraSpinner, logErrorAndExit, } from '../console/logging.js';
3
3
  import { getLocaleProperties } from 'generaltranslation';
4
+ import { getAuthHeaders } from '../utils/headers.js';
4
5
  /**
5
6
  * Waits for translations to be deployed to the General Translation API
6
7
  * @param apiKey - The API key for the General Translation API
@@ -11,7 +12,7 @@ import { getLocaleProperties } from 'generaltranslation';
11
12
  * @param timeoutDuration - The timeout duration for the wait
12
13
  * @returns True if all translations are deployed, false otherwise
13
14
  */
14
- export const waitForUpdates = async (apiKey, baseUrl, versionId, startTime, timeoutDuration) => {
15
+ export const waitForUpdates = async (projectId, apiKey, baseUrl, versionId, startTime, timeoutDuration) => {
15
16
  console.log();
16
17
  const spinner = await createOraSpinner();
17
18
  spinner.start('Waiting for translation...');
@@ -21,7 +22,7 @@ export const waitForUpdates = async (apiKey, baseUrl, versionId, startTime, time
21
22
  method: 'GET',
22
23
  headers: {
23
24
  'Content-Type': 'application/json',
24
- ...(apiKey && { 'x-gt-api-key': apiKey }),
25
+ ...getAuthHeaders(projectId, apiKey),
25
26
  },
26
27
  });
27
28
  if (response.ok) {
package/dist/cli/react.js CHANGED
@@ -16,6 +16,8 @@ import updateConfig from '../fs/config/updateConfig.js';
16
16
  import { validateConfigExists } from '../config/validateSettings.js';
17
17
  import { validateProject } from '../translation/validate.js';
18
18
  import { intro } from '@clack/prompts';
19
+ import localizeStaticUrls from '../utils/localizeStaticUrls.js';
20
+ import flattenJsonFiles from '../utils/flattenJsonFiles.js';
19
21
  const DEFAULT_TIMEOUT = 600;
20
22
  const pkg = 'gt-react';
21
23
  export class ReactCLI extends BaseCLI {
@@ -75,6 +77,9 @@ export class ReactCLI extends BaseCLI {
75
77
  .option('--ignore-errors', 'Ignore errors encountered while scanning for <T> tags', false)
76
78
  .option('--dry-run', 'Dry run, does not send updates to General Translation API', false)
77
79
  .option('--timeout <seconds>', 'Timeout in seconds for waiting for updates to be deployed to the CDN', DEFAULT_TIMEOUT.toString())
80
+ .option('--experimental-localize-static-urls', 'Triggering this will run a script after the cli tool that localizes all urls in content files. Currently only supported for md and mdx files.', false)
81
+ .option('--experimental-hide-default-locale', 'When localizing static locales, hide the default locale from the path', false)
82
+ .option('--experimental-flatten-json-files', 'Triggering this will flatten the json files into a single file. This is useful for projects that have a lot of json files.', false)
78
83
  .action(async (options) => {
79
84
  displayHeader('Translating project...');
80
85
  await this.handleTranslate(options);
@@ -262,7 +267,7 @@ export class ReactCLI extends BaseCLI {
262
267
  await super.handleGenericTranslate(options);
263
268
  // If the base class's handleTranslate completes successfully, continue with ReactCLI-specific code
264
269
  }
265
- catch (error) {
270
+ catch {
266
271
  // Continue with ReactCLI-specific code even if base handleTranslate failed
267
272
  }
268
273
  if (!settings.stageTranslations) {
@@ -280,6 +285,14 @@ export class ReactCLI extends BaseCLI {
280
285
  }
281
286
  await translate(options, settings._versionId);
282
287
  }
288
+ // Localize static urls (/docs -> /[locale]/docs)
289
+ if (options.experimentalLocalizeStaticUrls) {
290
+ await localizeStaticUrls(options);
291
+ }
292
+ // Flatten json files into a single file
293
+ if (options.experimentalFlattenJsonFiles) {
294
+ await flattenJsonFiles(options);
295
+ }
283
296
  }
284
297
  async handleValidate(initOptions) {
285
298
  validateConfigExists();
@@ -1,5 +1,5 @@
1
- import { displayProjectId, warnApiKeyInConfig } from '../console/logging.js';
2
- import loadConfig from '../fs/config/loadConfig.js';
1
+ import { displayProjectId, logErrorAndExit, warnApiKeyInConfig, } from '../console/logging.js';
2
+ import { loadConfig } from '../fs/config/loadConfig.js';
3
3
  import { defaultBaseUrl, libraryDefaultLocale, } from 'generaltranslation/internal';
4
4
  import fs from 'node:fs';
5
5
  import { createOrUpdateConfig } from '../fs/config/setupConfig.js';
@@ -8,6 +8,8 @@ import { validateSettings } from './validateSettings.js';
8
8
  import { GT_DASHBOARD_URL } from '../utils/constants.js';
9
9
  import { resolveProjectId } from '../fs/utils.js';
10
10
  import path from 'node:path';
11
+ import chalk from 'chalk';
12
+ import { resolveConfig } from './resolveConfig.js';
11
13
  export const DEFAULT_SRC_PATTERNS = [
12
14
  'src/**/*.{js,jsx,ts,tsx}',
13
15
  'app/**/*.{js,jsx,ts,tsx}',
@@ -29,23 +31,33 @@ export async function generateSettings(options, cwd = process.cwd()) {
29
31
  if (options.config) {
30
32
  gtConfig = loadConfig(options.config);
31
33
  }
32
- else if (fs.existsSync(path.join(cwd, 'gt.config.json'))) {
33
- options.config = path.join(cwd, 'gt.config.json');
34
- gtConfig = loadConfig(options.config);
35
- }
36
- else if (fs.existsSync(path.join(cwd, 'src/gt.config.json'))) {
37
- options.config = path.join(cwd, 'src/gt.config.json');
38
- gtConfig = loadConfig(options.config);
39
- }
40
34
  else {
41
- // If neither config exists, use empty config
42
- gtConfig = {};
35
+ const config = resolveConfig(cwd);
36
+ if (config) {
37
+ gtConfig = config.config;
38
+ options.config = config.path;
39
+ }
40
+ else {
41
+ gtConfig = {};
42
+ }
43
43
  }
44
44
  // Warn if apiKey is present in gt.config.json
45
45
  if (gtConfig.apiKey) {
46
46
  warnApiKeyInConfig(options.config);
47
47
  process.exit(1);
48
48
  }
49
+ const projectIdEnv = resolveProjectId();
50
+ // Resolve mismatched projectIds
51
+ if (gtConfig.projectId &&
52
+ options.projectId &&
53
+ gtConfig.projectId !== options.projectId) {
54
+ logErrorAndExit(`Project ID mismatch between ${chalk.green(gtConfig.projectId)} and ${chalk.green(options.projectId)}! Please use the same projectId in all configs.`);
55
+ }
56
+ else if (gtConfig.projectId &&
57
+ projectIdEnv &&
58
+ gtConfig.projectId !== projectIdEnv) {
59
+ logErrorAndExit(`Project ID mismatch between ${chalk.green(gtConfig.projectId)} and ${chalk.green(projectIdEnv)}! Please use the same projectId in all configs.`);
60
+ }
49
61
  // merge options
50
62
  const mergedOptions = { ...gtConfig, ...options };
51
63
  // merge locales
@@ -0,0 +1,4 @@
1
+ export declare function resolveConfig(cwd: string): {
2
+ path: string;
3
+ config: Record<string, any>;
4
+ } | null;
@@ -0,0 +1,19 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadConfig } from '../fs/config/loadConfig.js';
4
+ export function resolveConfig(cwd) {
5
+ const configFilepath = 'gt.config.json';
6
+ if (fs.existsSync(path.join(cwd, configFilepath))) {
7
+ return {
8
+ path: path.join(cwd, configFilepath),
9
+ config: loadConfig(path.join(cwd, configFilepath)),
10
+ };
11
+ }
12
+ if (fs.existsSync(path.join(cwd, 'src/gt.config.json'))) {
13
+ return {
14
+ path: path.join(cwd, 'src/gt.config.json'),
15
+ config: loadConfig(path.join(cwd, 'src/gt.config.json')),
16
+ };
17
+ }
18
+ return null;
19
+ }
@@ -11,3 +11,7 @@ import { TranslateOptions } from '../../cli/base.js';
11
11
  * @returns Promise that resolves when translation is complete
12
12
  */
13
13
  export declare function translateFiles(filePaths: ResolvedFiles, placeholderPaths: ResolvedFiles, transformPaths: TransformFiles, dataFormat: DataFormat | undefined, options: Settings & TranslateOptions): Promise<void>;
14
+ /**
15
+ * Creates a mapping between source files and their translated counterparts for each locale
16
+ */
17
+ export declare function createFileMapping(filePaths: ResolvedFiles, placeholderPaths: ResolvedFiles, transformPaths: TransformFiles, locales: string[]): Record<string, Record<string, string>>;
@@ -10,6 +10,7 @@ import chalk from 'chalk';
10
10
  import { downloadFile } from '../../api/downloadFile.js';
11
11
  import { downloadFileBatch } from '../../api/downloadFileBatch.js';
12
12
  import { SUPPORTED_FILE_EXTENSIONS } from './supportedFiles.js';
13
+ import sanitizeFileContent from '../../utils/sanitizeFileContent.js';
13
14
  const SUPPORTED_DATA_FORMATS = ['JSX', 'ICU', 'I18NEXT'];
14
15
  /**
15
16
  * Sends multiple files to the API for translation
@@ -49,9 +50,10 @@ export async function translateFiles(filePaths, placeholderPaths, transformPaths
49
50
  if (filePaths[fileType]) {
50
51
  const files = filePaths[fileType].map((filePath) => {
51
52
  const content = readFile(filePath);
53
+ const sanitizedContent = sanitizeFileContent(content);
52
54
  const relativePath = getRelative(filePath);
53
55
  return {
54
- content,
56
+ content: sanitizedContent,
55
57
  fileName: relativePath,
56
58
  fileExtension: fileType.toUpperCase(),
57
59
  dataFormat,
@@ -98,7 +100,7 @@ export async function translateFiles(filePaths, placeholderPaths, transformPaths
98
100
  // Process any translations that were already completed and returned with the initial response
99
101
  const downloadStatus = await processInitialTranslations(translations, fileMapping, options);
100
102
  // Check for remaining translations
101
- await checkFileTranslations(options.apiKey, options.baseUrl, data, locales, 600, (sourcePath, locale) => fileMapping[locale][sourcePath], downloadStatus // Pass the already downloaded files to avoid duplicate requests
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
102
104
  );
103
105
  }
104
106
  catch (error) {
@@ -108,7 +110,7 @@ export async function translateFiles(filePaths, placeholderPaths, transformPaths
108
110
  /**
109
111
  * Creates a mapping between source files and their translated counterparts for each locale
110
112
  */
111
- function createFileMapping(filePaths, placeholderPaths, transformPaths, locales) {
113
+ export function createFileMapping(filePaths, placeholderPaths, transformPaths, locales) {
112
114
  const fileMapping = {};
113
115
  for (const locale of locales) {
114
116
  const translatedPaths = resolveLocaleFiles(placeholderPaths, locale);
@@ -180,7 +182,7 @@ async function processInitialTranslations(translations = [], fileMapping, option
180
182
  }
181
183
  // Use batch download if there are multiple files
182
184
  if (batchFiles.length > 1) {
183
- const batchResult = await downloadFileBatch(options.baseUrl, options.apiKey, batchFiles.map(({ translationId, outputPath }) => ({
185
+ const batchResult = await downloadFileBatch(options.baseUrl, options.projectId, options.apiKey, batchFiles.map(({ translationId, outputPath }) => ({
184
186
  translationId,
185
187
  outputPath,
186
188
  })));
@@ -198,7 +200,7 @@ async function processInitialTranslations(translations = [], fileMapping, option
198
200
  else if (batchFiles.length === 1) {
199
201
  // For a single file, use the original downloadFile method
200
202
  const file = batchFiles[0];
201
- const result = await downloadFile(options.baseUrl, options.apiKey, file.translationId, file.outputPath);
203
+ const result = await downloadFile(options.baseUrl, options.projectId, options.apiKey, file.translationId, file.outputPath);
202
204
  if (result) {
203
205
  downloadStatus.downloaded.add(file.fileLocale);
204
206
  }
@@ -1 +1 @@
1
- export default function loadConfig(filepath: string): Record<string, any>;
1
+ export declare function loadConfig(filepath: string): Record<string, any>;
@@ -1,5 +1,5 @@
1
1
  import fs from 'node:fs';
2
- export default function loadConfig(filepath) {
2
+ export function loadConfig(filepath) {
3
3
  try {
4
4
  return JSON.parse(fs.readFileSync(filepath, 'utf-8'));
5
5
  }
@@ -92,29 +92,30 @@ function expandGlobPatterns(cwd, includePatterns, excludePatterns, locale, trans
92
92
  resolvedPaths.push(...matches);
93
93
  // For each match, create a version with [locale] in the correct positions
94
94
  matches.forEach((match) => {
95
- // Convert to relative path to make replacement easier
96
- const relativePath = path.relative(cwd, match);
97
- let originalRelativePath = relativePath;
98
- // Replace locale with [locale] at each tracked position
95
+ // Convert to absolute path to make replacement easier
96
+ const absolutePath = path.resolve(cwd, match);
97
+ const patternPath = path.resolve(cwd, pattern);
98
+ let originalAbsolutePath = absolutePath;
99
99
  if (localePositions.length > 0) {
100
- // We need to account for path resolution differences
101
- // This is a simplified approach - we'll replace all instances of the locale
100
+ // Replace all instances of [locale]
102
101
  // but only in path segments where we expect it based on the original pattern
103
- const pathParts = relativePath.split(path.sep);
104
- const patternParts = pattern.split(/[\/\\]/); // Handle both slash types
102
+ const pathParts = absolutePath.split(path.sep);
103
+ const patternParts = patternPath.split(path.sep);
105
104
  for (let i = 0; i < pathParts.length; i++) {
106
105
  if (i < patternParts.length) {
107
106
  if (patternParts[i].includes(localeTag)) {
108
107
  // This segment should have the locale replaced
109
- pathParts[i] = pathParts[i].replace(locale, localeTag);
108
+ // Create regex from pattern to match the actual path structure
109
+ const regexPattern = patternParts[i].replace(/\[locale\]/g, `(${locale.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`);
110
+ const regex = new RegExp(regexPattern);
111
+ pathParts[i] = pathParts[i].replace(regex, patternParts[i].replace(/\[locale\]/g, localeTag));
110
112
  }
111
113
  }
112
114
  }
113
- originalRelativePath = pathParts.join(path.sep);
115
+ originalAbsolutePath = pathParts.join(path.sep);
114
116
  }
115
117
  // Convert back to absolute path
116
- const originalPath = path.resolve(cwd, originalRelativePath);
117
- placeholderPaths.push(originalPath);
118
+ placeholderPaths.push(originalAbsolutePath);
118
119
  });
119
120
  }
120
121
  return { resolvedPaths, placeholderPaths };
@@ -12,4 +12,5 @@ export declare function createOrUpdateConfig(configFilepath: string, options: {
12
12
  locales?: string[];
13
13
  files?: FilesOptions;
14
14
  framework?: SupportedFrameworks;
15
+ baseUrl?: string;
15
16
  }): Promise<string>;
@@ -15,6 +15,7 @@ export async function createOrUpdateConfig(configFilepath, options) {
15
15
  ...(options.defaultLocale && { defaultLocale: options.defaultLocale }),
16
16
  ...(options.files && { files: options.files }),
17
17
  ...(options.framework && { framework: options.framework }),
18
+ ...(options.baseUrl && { baseUrl: options.baseUrl }),
18
19
  };
19
20
  try {
20
21
  // if file exists
@@ -6,11 +6,11 @@ export async function translate(settings, versionId) {
6
6
  // timeout was validated earlier
7
7
  const startTime = Date.now();
8
8
  const timeout = parseInt(settings.timeout) * 1000;
9
- const result = await waitForUpdates(settings.apiKey, settings.baseUrl, versionId, startTime, timeout);
9
+ const result = await waitForUpdates(settings.projectId, settings.apiKey, settings.baseUrl, versionId, startTime, timeout);
10
10
  if (!result) {
11
11
  process.exit(1);
12
12
  }
13
- const translations = await fetchTranslations(settings.baseUrl, settings.apiKey, versionId);
13
+ const translations = await fetchTranslations(settings.baseUrl, settings.projectId, settings.apiKey, versionId);
14
14
  // Save translations to local directory if files.gt.output is provided
15
15
  if (settings.files && isUsingLocalTranslations(settings)) {
16
16
  await saveTranslations(translations, settings.files.placeholderPaths, 'JSX');
@@ -28,6 +28,9 @@ export type Options = {
28
28
  dryRun: boolean;
29
29
  timeout: string;
30
30
  stageTranslations?: boolean;
31
+ experimentalLocalizeStaticUrls?: boolean;
32
+ experimentalHideDefaultLocale?: boolean;
33
+ experimentalFlattenJsonFiles?: boolean;
31
34
  };
32
35
  export type WrapOptions = {
33
36
  src?: string[];
@@ -0,0 +1,2 @@
1
+ import { Settings, Options } from '../types/index.js';
2
+ export default function flattenJsonFiles(settings: Settings & Options): Promise<void>;
@@ -0,0 +1,36 @@
1
+ import { createFileMapping } from '../formats/files/translate.js';
2
+ import fs from 'node:fs';
3
+ export default async function flattenJsonFiles(settings) {
4
+ if (!settings.files ||
5
+ (Object.keys(settings.files.placeholderPaths).length === 1 &&
6
+ settings.files.placeholderPaths.gt)) {
7
+ return;
8
+ }
9
+ const { resolvedPaths: sourceFiles } = settings.files;
10
+ const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales);
11
+ await Promise.all(Object.values(fileMapping).map(async (filesMap) => {
12
+ const targetFiles = Object.values(filesMap).filter((path) => path.endsWith('.json'));
13
+ await Promise.all(targetFiles.map(async (file) => {
14
+ // Read each json file
15
+ const json = JSON.parse(fs.readFileSync(file, 'utf8'));
16
+ // Flatten the json
17
+ const flattenedJson = flattenJson(json);
18
+ // Write the flattened json to the target file
19
+ await fs.promises.writeFile(file, JSON.stringify(flattenedJson, null, 2));
20
+ return flattenedJson;
21
+ }));
22
+ }));
23
+ }
24
+ function flattenJson(json, prefix = '') {
25
+ const result = {};
26
+ for (const [key, value] of Object.entries(json)) {
27
+ const newKey = prefix ? `${prefix}.${key}` : key;
28
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
29
+ Object.assign(result, flattenJson(value, newKey));
30
+ }
31
+ else {
32
+ result[newKey] = value;
33
+ }
34
+ }
35
+ return result;
36
+ }
@@ -0,0 +1 @@
1
+ export declare function getAuthHeaders(projectId: string, apiKey: string): Record<string, string>;
@@ -0,0 +1,14 @@
1
+ export function getAuthHeaders(projectId, apiKey) {
2
+ const authHeaders = {
3
+ 'x-gt-project-id': projectId,
4
+ };
5
+ if (apiKey) {
6
+ if (apiKey.startsWith('gtx-internal-')) {
7
+ authHeaders['x-gt-internal-api-key'] = apiKey;
8
+ }
9
+ else {
10
+ authHeaders['x-gt-api-key'] = apiKey;
11
+ }
12
+ }
13
+ return authHeaders;
14
+ }
@@ -0,0 +1,15 @@
1
+ import { Options, Settings } from '../types/index.js';
2
+ /**
3
+ * Localizes static urls in content files.
4
+ * Currently only supported for md and mdx files. (/docs/ -> /[locale]/docs/)
5
+ * @param settings - The settings object containing the project configuration.
6
+ * @returns void
7
+ *
8
+ * @TODO This is an experimental feature, and only works in very specific cases. This needs to be improved before
9
+ * it can be enabled by default.
10
+ *
11
+ * Before this becomes a non-experimental feature, we need to:
12
+ * - Support more file types
13
+ * - Support more complex paths
14
+ */
15
+ export default function localizeStaticUrls(settings: Settings & Options): Promise<void>;
@@ -0,0 +1,78 @@
1
+ import * as fs from 'fs';
2
+ import { createFileMapping } from '../formats/files/translate.js';
3
+ /**
4
+ * Localizes static urls in content files.
5
+ * Currently only supported for md and mdx files. (/docs/ -> /[locale]/docs/)
6
+ * @param settings - The settings object containing the project configuration.
7
+ * @returns void
8
+ *
9
+ * @TODO This is an experimental feature, and only works in very specific cases. This needs to be improved before
10
+ * it can be enabled by default.
11
+ *
12
+ * Before this becomes a non-experimental feature, we need to:
13
+ * - Support more file types
14
+ * - Support more complex paths
15
+ */
16
+ export default async function localizeStaticUrls(settings) {
17
+ if (!settings.files ||
18
+ (Object.keys(settings.files.placeholderPaths).length === 1 &&
19
+ settings.files.placeholderPaths.gt)) {
20
+ return;
21
+ }
22
+ const { resolvedPaths: sourceFiles } = settings.files;
23
+ const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales);
24
+ // Process all file types at once with a single call
25
+ await Promise.all(Object.entries(fileMapping).map(async ([locale, filesMap]) => {
26
+ // Get all files that are md or mdx
27
+ const targetFiles = Object.values(filesMap).filter((path) => path.endsWith('.md') || path.endsWith('.mdx'));
28
+ // Replace the placeholder path with the target path
29
+ await Promise.all(targetFiles.map(async (filePath) => {
30
+ // Get file content
31
+ const fileContent = fs.readFileSync(filePath, 'utf8');
32
+ // Localize the file
33
+ const localizedFile = localizeStaticUrlsForFile(fileContent, settings.defaultLocale, locale, settings.experimentalHideDefaultLocale || false);
34
+ // Write the localized file to the target path
35
+ await fs.promises.writeFile(filePath, localizedFile);
36
+ }));
37
+ }));
38
+ }
39
+ // Assumption: we will be seeing localized paths in the source files: (docs/en/ -> docs/ja/)
40
+ function localizeStaticUrlsForFile(file, defaultLocale, targetLocale, hideDefaultLocale) {
41
+ // 1. Search for all instances of:
42
+ let regex;
43
+ if (hideDefaultLocale) {
44
+ // Match complete markdown links: `](/docs/...)` or `](/docs)`
45
+ regex = new RegExp(`\\]\\(/docs(?:/([^)]*))?\\)`, 'g');
46
+ }
47
+ else {
48
+ // Match complete markdown links with default locale: `](/docs/${defaultLocale}/...)` or `](/docs/${defaultLocale})`
49
+ regex = new RegExp(`\\]\\(/docs/${defaultLocale}(?:/([^)]*))?\\)`, 'g');
50
+ }
51
+ const matches = file.match(regex);
52
+ if (!matches) {
53
+ return file;
54
+ }
55
+ // 2. Replace the default locale with the target locale in all matched instances
56
+ const localizedFile = file.replace(regex, (match, pathContent) => {
57
+ if (hideDefaultLocale) {
58
+ // For hideDefaultLocale, check if path already has target locale
59
+ if (pathContent) {
60
+ if (pathContent.startsWith(`${targetLocale}/`) ||
61
+ pathContent === targetLocale) {
62
+ return match; // Already localized
63
+ }
64
+ }
65
+ // Add target locale to the path
66
+ if (!pathContent || pathContent === '') {
67
+ return `](/docs/${targetLocale})`;
68
+ }
69
+ return `](/docs/${targetLocale}/${pathContent})`;
70
+ }
71
+ else {
72
+ // For non-hideDefaultLocale, replace defaultLocale with targetLocale
73
+ // pathContent contains everything after the default locale (no leading slash if present)
74
+ return `](/docs/${targetLocale}${pathContent ? '/' + pathContent : ''})`;
75
+ }
76
+ });
77
+ return localizedFile;
78
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Processes content to escape curl commands within tick marks and existing escape strings
3
+ * @param content - The content to process
4
+ * @returns the processed content with escaped curl commands
5
+ */
6
+ export default function sanitizeFileContent(content: string): string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Processes content to escape curl commands within tick marks and existing escape strings
3
+ * @param content - The content to process
4
+ * @returns the processed content with escaped curl commands
5
+ */
6
+ export default function sanitizeFileContent(content) {
7
+ const ESCAPE_STRING = '_GT_INTERNAL_ESCAPE';
8
+ const allTickMarkRegex = /`([^`]*)`/g;
9
+ let processedContent = content;
10
+ // First, escape any existing tick marks followed by _GT_INTERNAL_ESCAPE
11
+ // This protects pre-existing escapes
12
+ processedContent = processedContent.replace(new RegExp('`' + ESCAPE_STRING, 'g'), '`' + ESCAPE_STRING + ESCAPE_STRING);
13
+ // Then find ALL tick mark pairs and process them individually
14
+ // This approach is more reliable than negative lookahead with modified content
15
+ processedContent = processedContent.replace(allTickMarkRegex, (match, innerContent) => {
16
+ // Skip if this already starts with our escape string (protected or already processed)
17
+ if (innerContent.startsWith(ESCAPE_STRING)) {
18
+ return match;
19
+ }
20
+ // Check if the content contains a curl command
21
+ if (/\bcurl\b/i.test(innerContent)) {
22
+ // Insert escape string after opening tick
23
+ return '`' + ESCAPE_STRING + innerContent + '`';
24
+ }
25
+ // Return original match if no curl command found
26
+ return match;
27
+ });
28
+ return processedContent;
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtx-cli",
3
- "version": "1.2.30-alpha.32",
3
+ "version": "1.2.30-alpha.34",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [
@@ -87,7 +87,7 @@
87
87
  "esbuild": "^0.25.4",
88
88
  "fast-glob": "^3.3.3",
89
89
  "form-data": "^4.0.2",
90
- "generaltranslation": "^7.0.0-alpha.32",
90
+ "generaltranslation": "^7.0.0-alpha.34",
91
91
  "open": "^10.1.1",
92
92
  "ora": "^8.2.0",
93
93
  "resolve": "^1.22.10",