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 +12 -0
- package/dist/cli/react.js +14 -1
- package/dist/formats/files/translate.d.ts +4 -0
- package/dist/formats/files/translate.js +4 -2
- package/dist/fs/config/parseFilesConfig.js +13 -12
- package/dist/types/index.d.ts +3 -0
- package/dist/utils/flattenJsonFiles.d.ts +2 -0
- package/dist/utils/flattenJsonFiles.js +36 -0
- package/dist/utils/localizeStaticUrls.d.ts +15 -0
- package/dist/utils/localizeStaticUrls.js +78 -0
- package/dist/utils/sanitizeFileContent.d.ts +6 -0
- package/dist/utils/sanitizeFileContent.js +29 -0
- package/package.json +1 -1
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
|
|
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
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
//
|
|
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 =
|
|
104
|
-
const patternParts =
|
|
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
|
-
|
|
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
|
-
|
|
115
|
+
originalAbsolutePath = pathParts.join(path.sep);
|
|
114
116
|
}
|
|
115
117
|
// Convert back to absolute path
|
|
116
|
-
|
|
117
|
-
placeholderPaths.push(originalPath);
|
|
118
|
+
placeholderPaths.push(originalAbsolutePath);
|
|
118
119
|
});
|
|
119
120
|
}
|
|
120
121
|
return { resolvedPaths, placeholderPaths };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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,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
|
+
}
|