gtx-cli 1.2.31 → 1.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## 1.2.31
4
16
 
5
17
  ### Patch Changes
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();
@@ -12,3 +12,7 @@ import { TranslateOptions } from '../../cli/base.js';
12
12
  * @returns Promise that resolves when translation is complete
13
13
  */
14
14
  export declare function translateFiles(filePaths: ResolvedFiles, placeholderPaths: ResolvedFiles, transformPaths: TransformFiles, dataFormat: DataFormat | undefined, options: Settings & TranslateOptions): Promise<void>;
15
+ /**
16
+ * Creates a mapping between source files and their translated counterparts for each locale
17
+ */
18
+ 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
@@ -50,9 +51,10 @@ export async function translateFiles(filePaths, placeholderPaths, transformPaths
50
51
  if (filePaths[fileType]) {
51
52
  const files = filePaths[fileType].map((filePath) => {
52
53
  const content = readFile(filePath);
54
+ const sanitizedContent = sanitizeFileContent(content);
53
55
  const relativePath = getRelative(filePath);
54
56
  return {
55
- content,
57
+ content: sanitizedContent,
56
58
  fileName: relativePath,
57
59
  fileFormat: fileType.toUpperCase(),
58
60
  dataFormat,
@@ -109,7 +111,7 @@ export async function translateFiles(filePaths, placeholderPaths, transformPaths
109
111
  /**
110
112
  * Creates a mapping between source files and their translated counterparts for each locale
111
113
  */
112
- function createFileMapping(filePaths, placeholderPaths, transformPaths, locales) {
114
+ export function createFileMapping(filePaths, placeholderPaths, transformPaths, locales) {
113
115
  const fileMapping = {};
114
116
  for (const locale of locales) {
115
117
  const translatedPaths = resolveLocaleFiles(placeholderPaths, locale);
@@ -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 };
@@ -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,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.31",
3
+ "version": "1.2.34",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [