gtx-cli 2.0.9 → 2.0.11

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
+ ## 2.0.11
4
+
5
+ ### Patch Changes
6
+
7
+ - [#501](https://github.com/generaltranslation/gt/pull/501) [`d353c84`](https://github.com/generaltranslation/gt/commit/d353c84aaa159dbc77cff3ac29953adef4c64597) Thanks [@ErnestM1234](https://github.com/ErnestM1234)! - feat: add localization for href
8
+
9
+ ## 2.0.10
10
+
11
+ ### Patch Changes
12
+
13
+ - [#499](https://github.com/generaltranslation/gt/pull/499) [`0793ef7`](https://github.com/generaltranslation/gt/commit/0793ef7f0d5b391805d072ff0c251fe43fa58b29) Thanks [@ErnestM1234](https://github.com/ErnestM1234)! - feat: add localization for imports
14
+
3
15
  ## 2.0.9
4
16
 
5
17
  ### Patch Changes
@@ -10,6 +10,7 @@ export type TranslateOptions = {
10
10
  experimentalLocalizeStaticUrls?: boolean;
11
11
  experimentalHideDefaultLocale?: boolean;
12
12
  experimentalFlattenJsonFiles?: boolean;
13
+ experimentalLocalizeStaticImports?: boolean;
13
14
  };
14
15
  export type LoginOptions = {
15
16
  keyType?: 'development' | 'production';
package/dist/cli/base.js CHANGED
@@ -16,6 +16,8 @@ import { retrieveCredentials, setCredentials } from '../utils/credentials.js';
16
16
  import { areCredentialsSet } from '../utils/credentials.js';
17
17
  import localizeStaticUrls from '../utils/localizeStaticUrls.js';
18
18
  import flattenJsonFiles from '../utils/flattenJsonFiles.js';
19
+ import localizeStaticImports from '../utils/localizeStaticImports.js';
20
+ import copyFile from '../fs/copyFile.js';
19
21
  export class BaseCLI {
20
22
  library;
21
23
  additionalModules;
@@ -54,6 +56,7 @@ export class BaseCLI {
54
56
  .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)
55
57
  .option('--experimental-hide-default-locale', 'When localizing static locales, hide the default locale from the path', false)
56
58
  .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)
59
+ .option('--experimental-localize-static-imports', 'Triggering this will run a script after the cli tool that localizes all static imports in content files. Currently only supported for md and mdx files.', false)
57
60
  .action(async (initOptions) => {
58
61
  displayHeader('Starting translation...');
59
62
  const settings = await generateSettings(initOptions);
@@ -181,10 +184,18 @@ See the docs for more information: https://generaltranslation.com/docs/react/tut
181
184
  if (settings.experimentalLocalizeStaticUrls) {
182
185
  await localizeStaticUrls(settings);
183
186
  }
187
+ // Localize static imports (/docs -> /[locale]/docs)
188
+ if (settings.experimentalLocalizeStaticImports) {
189
+ await localizeStaticImports(settings);
190
+ }
184
191
  // Flatten json files into a single file
185
192
  if (settings.experimentalFlattenJsonFiles) {
186
193
  await flattenJsonFiles(settings);
187
194
  }
195
+ // Copy files to the target locale
196
+ if (settings.options?.copyFiles) {
197
+ await copyFile(settings);
198
+ }
188
199
  }
189
200
  async handleSetupReactCommand(options) {
190
201
  await handleSetupReactCommand(options);
package/dist/cli/react.js CHANGED
@@ -78,6 +78,7 @@ export class ReactCLI extends BaseCLI {
78
78
  .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)
79
79
  .option('--experimental-hide-default-locale', 'When localizing static locales, hide the default locale from the path', false)
80
80
  .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)
81
+ .option('--experimental-localize-static-imports', 'Triggering this will run a script after the cli tool that localizes all static imports in content files. Currently only supported for md and mdx files.', false)
81
82
  .action(async (options) => {
82
83
  displayHeader('Translating project...');
83
84
  await this.handleTranslate(options);
@@ -64,6 +64,17 @@ export async function generateSettings(options, cwd = process.cwd()) {
64
64
  !options.options?.docsUrlPattern.includes('[locale]')) {
65
65
  logErrorAndExit('Failed to localize static urls: URL pattern must include "[locale]" to denote the location of the locale');
66
66
  }
67
+ if (options.options?.docsImportPattern &&
68
+ !options.options?.docsImportPattern.includes('[locale]')) {
69
+ logErrorAndExit('Failed to localize static imports: Import pattern must include "[locale]" to denote the location of the locale');
70
+ }
71
+ if (options.options?.copyFiles) {
72
+ for (const file of options.options.copyFiles) {
73
+ if (!file.includes('[locale]')) {
74
+ logErrorAndExit('Failed to copy files: File path must include "[locale]" to denote the location of the locale');
75
+ }
76
+ }
77
+ }
67
78
  // merge options
68
79
  const mergedOptions = { ...gtConfig, ...options };
69
80
  // merge locales
@@ -21,6 +21,22 @@ export function generatePreset(preset) {
21
21
  },
22
22
  },
23
23
  },
24
+ // Enable this when support multiple language objects in array
25
+ // '$.redirects': {
26
+ // type: 'array',
27
+ // key: '$.language',
28
+ // include: [],
29
+ // transform: {
30
+ // '$.source': {
31
+ // match: '^/{locale}/(.*)$',
32
+ // replace: '/{locale}/$1',
33
+ // },
34
+ // '$.destination': {
35
+ // match: '^/{locale}/(.*)$',
36
+ // replace: '/{locale}/$1',
37
+ // },
38
+ // },
39
+ // },
24
40
  },
25
41
  };
26
42
  default:
@@ -29,7 +29,7 @@ export function findMatchingItemArray(locale, sourceObjectOptions, sourceObjectP
29
29
  exit(1);
30
30
  }
31
31
  // Validate the key is the identifying locale property
32
- if (!keyCandidates[0] ||
32
+ if (!keyCandidates.length ||
33
33
  identifyingLocaleProperty !== keyCandidates[0].value) {
34
34
  continue;
35
35
  }
@@ -0,0 +1,8 @@
1
+ import { Settings } from '../types/index.js';
2
+ import { TranslateOptions } from '../cli/base.js';
3
+ /**
4
+ * Copy a file to target locale without translation
5
+ *
6
+ * This is a naive approach, does not allow for wild cards
7
+ */
8
+ export default function copyFile(settings: Settings & TranslateOptions): Promise<void>;
@@ -0,0 +1,39 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { logError } from '../console/logging.js';
4
+ /**
5
+ * Copy a file to target locale without translation
6
+ *
7
+ * This is a naive approach, does not allow for wild cards
8
+ */
9
+ export default async function copyFile(settings) {
10
+ if (!settings.options?.copyFiles || settings.options.copyFiles.length === 0) {
11
+ return;
12
+ }
13
+ // Construct a map of source paths to target paths
14
+ const copyFiles = settings.options.copyFiles.reduce((paths, filePathTemplate) => {
15
+ const sourcePath = path.join(process.cwd(), filePathTemplate.replace('[locale]', settings.defaultLocale));
16
+ if (!fs.existsSync(sourcePath)) {
17
+ logError(`Failed to copy files: File path does not exist: ${sourcePath}`);
18
+ return paths;
19
+ }
20
+ paths[sourcePath] = [];
21
+ for (const locale of settings.locales) {
22
+ if (locale === settings.defaultLocale)
23
+ continue;
24
+ const targetPath = path.join(process.cwd(), filePathTemplate.replace('[locale]', locale));
25
+ paths[sourcePath].push(targetPath);
26
+ }
27
+ return paths;
28
+ }, {});
29
+ // Copy each file to the target locale
30
+ for (const sourcePath in copyFiles) {
31
+ for (const targetPath of copyFiles[sourcePath]) {
32
+ // Ensure the target directory exists
33
+ const targetDir = path.dirname(targetPath);
34
+ await fs.promises.mkdir(targetDir, { recursive: true });
35
+ // Copy the file
36
+ await fs.promises.copyFile(sourcePath, targetPath);
37
+ }
38
+ }
39
+ }
@@ -20,6 +20,7 @@ export type Options = {
20
20
  experimentalLocalizeStaticUrls?: boolean;
21
21
  experimentalHideDefaultLocale?: boolean;
22
22
  experimentalFlattenJsonFiles?: boolean;
23
+ experimentalLocalizeStaticImports?: boolean;
23
24
  };
24
25
  export type WrapOptions = {
25
26
  src?: string[];
@@ -99,6 +100,9 @@ export type AdditionalOptions = {
99
100
  [fileGlob: string]: JsonSchema;
100
101
  };
101
102
  docsUrlPattern?: string;
103
+ docsImportPattern?: string;
104
+ docsHideDefaultLocaleImport?: boolean;
105
+ copyFiles?: string[];
102
106
  };
103
107
  export type JsonSchema = {
104
108
  preset?: 'mintlify';
@@ -112,10 +116,11 @@ export type SourceObjectOptions = {
112
116
  include: string[];
113
117
  key?: string;
114
118
  localeProperty?: string;
115
- transform?: {
116
- [transformPath: string]: {
117
- match?: string;
118
- replace: string;
119
- };
119
+ transform?: TransformOptions;
120
+ };
121
+ export type TransformOptions = {
122
+ [transformPath: string]: {
123
+ match?: string;
124
+ replace: string;
120
125
  };
121
126
  };
@@ -0,0 +1,15 @@
1
+ import { Options, Settings } from '../types/index.js';
2
+ /**
3
+ * Localizes static imports 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 localizeStaticImports(settings: Omit<Settings & Options, 'ignoreErrors' | 'suppressWarnings' | 'timeout'>): Promise<void>;
@@ -0,0 +1,93 @@
1
+ import * as fs from 'fs';
2
+ import { createFileMapping } from '../formats/files/translate.js';
3
+ import { logError } from '../console/logging.js';
4
+ /**
5
+ * Localizes static imports in content files.
6
+ * Currently only supported for md and mdx files. (/docs/ -> /[locale]/docs/)
7
+ * @param settings - The settings object containing the project configuration.
8
+ * @returns void
9
+ *
10
+ * @TODO This is an experimental feature, and only works in very specific cases. This needs to be improved before
11
+ * it can be enabled by default.
12
+ *
13
+ * Before this becomes a non-experimental feature, we need to:
14
+ * - Support more file types
15
+ * - Support more complex paths
16
+ */
17
+ export default async function localizeStaticImports(settings) {
18
+ if (!settings.files ||
19
+ (Object.keys(settings.files.placeholderPaths).length === 1 &&
20
+ settings.files.placeholderPaths.gt)) {
21
+ return;
22
+ }
23
+ const { resolvedPaths: sourceFiles } = settings.files;
24
+ const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales);
25
+ // Process all file types at once with a single call
26
+ await Promise.all(Object.entries(fileMapping).map(async ([locale, filesMap]) => {
27
+ // Get all files that are md or mdx
28
+ const targetFiles = Object.values(filesMap).filter((path) => path.endsWith('.md') || path.endsWith('.mdx'));
29
+ // Replace the placeholder path with the target path
30
+ await Promise.all(targetFiles.map(async (filePath) => {
31
+ // Get file content
32
+ const fileContent = await fs.promises.readFile(filePath, 'utf8');
33
+ // Localize the file
34
+ const localizedFile = localizeStaticImportsForFile(fileContent, settings.defaultLocale, locale, settings.options?.docsHideDefaultLocaleImport || false, settings.options?.docsImportPattern);
35
+ // Write the localized file to the target path
36
+ await fs.promises.writeFile(filePath, localizedFile);
37
+ }));
38
+ }));
39
+ }
40
+ // Naive find and replace, in the future, construct an AST
41
+ function localizeStaticImportsForFile(file, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]' // eg /docs/[locale] or /[locale]
42
+ ) {
43
+ if (!pattern.startsWith('/')) {
44
+ pattern = '/' + pattern;
45
+ }
46
+ // 1. Search for all instances of:
47
+ const patternHead = pattern.split('[locale]')[0];
48
+ let regex;
49
+ if (hideDefaultLocale) {
50
+ const trimmedPatternHead = patternHead.endsWith('/')
51
+ ? patternHead.slice(0, -1)
52
+ : patternHead;
53
+ // Match complete markdown links: `import { Foo } from '@/docs/[locale]/foo.md'`
54
+ regex = new RegExp(`import\\s+(.*?)\\s+from\\s+(["'])${trimmedPatternHead}(.*?)\\2`, 'g');
55
+ }
56
+ else {
57
+ // Match complete markdown links with default locale: `import { Foo } from '@/docs/${defaultLocale}/foo.md'`
58
+ regex = new RegExp(`import\\s+(.*?)\\s+from\\s+(["'])${patternHead}${defaultLocale}(.*?)\\2`, 'g');
59
+ }
60
+ const matches = file.match(regex);
61
+ if (!matches) {
62
+ return file;
63
+ }
64
+ // 2. Replace the default locale with the target locale in all matched instances
65
+ const localizedFile = file.replace(regex, (match, bindings, quoteType, pathContent) => {
66
+ // get the quote type
67
+ quoteType = match.match(/["']/)?.[0] || '"';
68
+ if (!quoteType) {
69
+ logError(`Failed to localize static imports: Import pattern must include quotes in ${pattern}`);
70
+ return match;
71
+ }
72
+ if (hideDefaultLocale) {
73
+ // For hideDefaultLocale, check if path already has target locale
74
+ if (pathContent) {
75
+ if (pathContent.startsWith(`${targetLocale}/`) ||
76
+ pathContent === targetLocale) {
77
+ return match; // Already localized
78
+ }
79
+ }
80
+ // Add target locale to the path
81
+ if (!pathContent || pathContent === '') {
82
+ return `import ${bindings} from ${quoteType}${patternHead}${targetLocale}${quoteType}`;
83
+ }
84
+ return `import ${bindings} from ${quoteType}${patternHead}${targetLocale}${pathContent}${quoteType}`;
85
+ }
86
+ else {
87
+ // For non-hideDefaultLocale, replace defaultLocale with targetLocale
88
+ // pathContent contains everything after the default locale (no leading slash if present)
89
+ return `import ${bindings} from ${quoteType}${patternHead}${targetLocale}${pathContent}${quoteType}`;
90
+ }
91
+ });
92
+ return localizedFile;
93
+ }
@@ -28,11 +28,12 @@ export default async function localizeStaticUrls(settings) {
28
28
  // Replace the placeholder path with the target path
29
29
  await Promise.all(targetFiles.map(async (filePath) => {
30
30
  // Get file content
31
- const fileContent = fs.readFileSync(filePath, 'utf8');
31
+ const fileContent = await fs.promises.readFile(filePath, 'utf8');
32
32
  // Localize the file
33
33
  const localizedFile = localizeStaticUrlsForFile(fileContent, settings.defaultLocale, locale, settings.experimentalHideDefaultLocale || false, settings.options?.docsUrlPattern);
34
+ const localizedFileHrefs = localizeStaticHrefsForFile(localizedFile, settings.defaultLocale, locale, settings.experimentalHideDefaultLocale || false, settings.options?.docsUrlPattern);
34
35
  // Write the localized file to the target path
35
- await fs.promises.writeFile(filePath, localizedFile);
36
+ await fs.promises.writeFile(filePath, localizedFileHrefs);
36
37
  }));
37
38
  }));
38
39
  }
@@ -44,14 +45,18 @@ function localizeStaticUrlsForFile(file, defaultLocale, targetLocale, hideDefaul
44
45
  }
45
46
  // 1. Search for all instances of:
46
47
  const patternHead = pattern.split('[locale]')[0];
48
+ // Escape special regex characters and remove trailing slash if present
49
+ const escapedPatternHead = patternHead
50
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
51
+ .replace(/\/$/, '');
47
52
  let regex;
48
53
  if (hideDefaultLocale) {
49
54
  // Match complete markdown links: `](/docs/...)` or `](/docs)`
50
- regex = new RegExp(`\\]\\(${patternHead}(?:/([^)]*))?\\)`, 'g');
55
+ regex = new RegExp(`\\]\\(${escapedPatternHead}(?:/([^)]*))?\\)`, 'g');
51
56
  }
52
57
  else {
53
58
  // Match complete markdown links with default locale: `](/docs/${defaultLocale}/...)` or `](/docs/${defaultLocale})`
54
- regex = new RegExp(`\\]\\(${patternHead}${defaultLocale}(?:/([^)]*))?\\)`, 'g');
59
+ regex = new RegExp(`\\]\\(${escapedPatternHead}/${defaultLocale}(?:/([^)]*))?\\)`, 'g');
55
60
  }
56
61
  const matches = file.match(regex);
57
62
  if (!matches) {
@@ -81,3 +86,51 @@ function localizeStaticUrlsForFile(file, defaultLocale, targetLocale, hideDefaul
81
86
  });
82
87
  return localizedFile;
83
88
  }
89
+ function localizeStaticHrefsForFile(file, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]' // eg /docs/[locale] or /[locale]
90
+ ) {
91
+ if (!pattern.startsWith('/')) {
92
+ pattern = '/' + pattern;
93
+ }
94
+ // 1. Search for all instances of:
95
+ const patternHead = pattern.split('[locale]')[0];
96
+ // Escape special regex characters and remove trailing slash if present
97
+ const escapedPatternHead = patternHead
98
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
99
+ .replace(/\/$/, '');
100
+ let regex;
101
+ if (hideDefaultLocale) {
102
+ // Match complete href attributes: `href="/docs/..."` or `href="/docs"`
103
+ regex = new RegExp(`href="${escapedPatternHead}(?:/([^"]*))?"`, 'g');
104
+ }
105
+ else {
106
+ // Match complete href attributes with default locale: `href="/docs/${defaultLocale}/..."` or `href="/docs/${defaultLocale}"`
107
+ regex = new RegExp(`href="${escapedPatternHead}/${defaultLocale}(?:/([^"]*))?"`, 'g');
108
+ }
109
+ const matches = file.match(regex);
110
+ if (!matches) {
111
+ return file;
112
+ }
113
+ // 2. Replace the default locale with the target locale in all matched instances
114
+ const localizedFile = file.replace(regex, (match, pathContent) => {
115
+ if (hideDefaultLocale) {
116
+ // For hideDefaultLocale, check if path already has target locale
117
+ if (pathContent) {
118
+ if (pathContent.startsWith(`${targetLocale}/`) ||
119
+ pathContent === targetLocale) {
120
+ return match; // Already localized
121
+ }
122
+ }
123
+ // Add target locale to the path
124
+ if (!pathContent || pathContent === '') {
125
+ return `href="${patternHead}${targetLocale}"`;
126
+ }
127
+ return `href="${patternHead}${targetLocale}/${pathContent}"`;
128
+ }
129
+ else {
130
+ // For non-hideDefaultLocale, replace defaultLocale with targetLocale
131
+ // pathContent contains everything after the default locale (no leading slash if present)
132
+ return `href="${patternHead}${targetLocale}${pathContent ? '/' + pathContent : ''}"`;
133
+ }
134
+ });
135
+ return localizedFile;
136
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtx-cli",
3
- "version": "2.0.9",
3
+ "version": "2.0.11",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [