gt 2.6.30-alpha.0 → 2.7.0

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,25 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1069](https://github.com/generaltranslation/gt/pull/1069) [`ff38c7c`](https://github.com/generaltranslation/gt/commit/ff38c7c72886882ddb8851fc8173e1ba863d0078) Thanks [@ErnestM1234](https://github.com/ErnestM1234)! - feat: add new gt package
8
+
9
+ ## 2.6.31
10
+
11
+ ### Patch Changes
12
+
13
+ - [#1070](https://github.com/generaltranslation/gt/pull/1070) [`516979d`](https://github.com/generaltranslation/gt/commit/516979d36cd16c4bc9080ea7dc06b7e299200919) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Handle case where all jobs fail
14
+
15
+ ## 2.6.30
16
+
17
+ ### Patch Changes
18
+
19
+ - [#1068](https://github.com/generaltranslation/gt/pull/1068) [`94b95ef`](https://github.com/generaltranslation/gt/commit/94b95ef662b81dac51416ecc64f3318339171f0b) Thanks [@ErnestM1234](https://github.com/ErnestM1234)! - fix: runtime calculation for the injection of 'data-' attribute in jsx
20
+
21
+ - [#1066](https://github.com/generaltranslation/gt/pull/1066) [`7b4837f`](https://github.com/generaltranslation/gt/commit/7b4837fe44e387c4de812d9b3f7fc394cb24e49e) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Wrapping text node URLs for Mintlify MDX to align with their parser
22
+
3
23
  ## 2.6.29
4
24
 
5
25
  ### Patch Changes
@@ -18,6 +18,7 @@ export declare const warnFunctionNotFoundSync: (file: string, functionName: stri
18
18
  export declare const warnInvalidDeclareVarNameSync: (file: string, value: string, location?: string) => string;
19
19
  export declare const warnDuplicateFunctionDefinitionSync: (file: string, functionName: string, location?: string) => string;
20
20
  export declare const warnInvalidStaticInitSync: (file: string, functionName: string, location?: string) => string;
21
+ export declare const warnDataAttrOnBranch: (file: string, attrName: string, location?: string) => string;
21
22
  export declare const warnRecursiveFunctionCallSync: (file: string, functionName: string, location?: string) => string;
22
23
  export declare const warnDeclareStaticNotWrappedSync: (file: string, functionName: string, location?: string) => string;
23
24
  export declare const warnDeclareStaticNoResultsSync: (file: string, functionName: string, location?: string) => string;
@@ -1,3 +1,4 @@
1
+ import { BRANCH_COMPONENT } from '../react/jsx/utils/constants.js';
1
2
  import { colorizeFilepath, colorizeComponent, colorizeIdString, colorizeContent, colorizeLine, colorizeFunctionName, } from './colors.js';
2
3
  import { formatCodeClamp } from './formatting.js';
3
4
  const withWillErrorInNextVersion = (message) => `${message} (This will become an error in the next major version of the CLI.)`;
@@ -28,6 +29,7 @@ export const warnDuplicateFunctionDefinitionSync = (file, functionName, location
28
29
  export const warnInvalidStaticInitSync = (file, functionName, location) => withLocation(file, withStaticError(`The definition for ${colorizeFunctionName(functionName)} could not be resolved. When using arrow syntax to define a static function, the right hand side or the assignment MUST only contain the arrow function itself and no other expressions.
29
30
  Example: ${colorizeContent(`const ${colorizeFunctionName(functionName)} = () => { ... }`)}
30
31
  Invalid: ${colorizeContent(`const ${colorizeFunctionName(functionName)} = [() => { ... }][0]`)}`), location);
32
+ export const warnDataAttrOnBranch = (file, attrName, location) => withLocation(file, `${colorizeComponent(`<${BRANCH_COMPONENT}>`)} component ignores attributes prefixed with ${colorizeIdString('"data-"')}. Found ${colorizeIdString(attrName)}. Remove it or use a different attribute name.`, location);
31
33
  export const warnRecursiveFunctionCallSync = (file, functionName, location) => withLocation(file, withStaticError(`Recursive function call detected: ${colorizeFunctionName(functionName)}. A static function cannot use recursive calls to construct its result.`), location);
32
34
  export const warnDeclareStaticNotWrappedSync = (file, functionName, location) => withLocation(file, withDeclareStaticError(`Could not resolve ${colorizeFunctionName(formatCodeClamp(functionName))}. This call is not wrapped in declareStatic(). Ensure the function is properly wrapped with declareStatic() and does not have circular import dependencies.`), location);
33
35
  export const warnDeclareStaticNoResultsSync = (file, functionName, location) => withLocation(file, withDeclareStaticError(`Could not resolve ${colorizeFunctionName(formatCodeClamp(functionName))}. DeclareStatic can only receive function invocations and cannot use undefined values or looped calls to construct its result.`), location);
@@ -2,14 +2,12 @@ import { logger } from '../../console/logger.js';
2
2
  import { recordWarning } from '../../state/translateWarnings.js';
3
3
  import { getRelative, readFile } from '../../fs/findFilepath.js';
4
4
  import { SUPPORTED_FILE_EXTENSIONS } from './supportedFiles.js';
5
- import sanitizeFileContent from '../../utils/sanitizeFileContent.js';
6
5
  import { parseJson } from '../json/parseJson.js';
7
6
  import parseYaml from '../yaml/parseYaml.js';
8
7
  import YAML from 'yaml';
9
8
  import { determineLibrary } from '../../fs/determineFramework.js';
10
- import { isValidMdx } from '../../utils/validateMdx.js';
11
9
  import { hashStringSync } from '../../utils/hash.js';
12
- import { applyMintlifyTitleFallback } from '../../utils/mintlifyTitleFallback.js';
10
+ import { preprocessContent } from './preprocessContent.js';
13
11
  export const SUPPORTED_DATA_FORMATS = ['JSX', 'ICU', 'I18NEXT'];
14
12
  export async function aggregateFiles(settings) {
15
13
  // Aggregate all files to translate
@@ -123,33 +121,18 @@ export async function aggregateFiles(settings) {
123
121
  .map((filePath) => {
124
122
  const content = readFile(filePath);
125
123
  const relativePath = getRelative(filePath);
126
- if (fileType === 'mdx') {
127
- if (!skipValidation?.mdx) {
128
- const validation = isValidMdx(content, filePath);
129
- if (!validation.isValid) {
130
- logger.warn(`Skipping ${relativePath}: MDX file is not AST parsable${validation.error ? `: ${validation.error}` : ''}`);
131
- recordWarning('skipped_file', relativePath, `MDX file is not AST parsable${validation.error ? `: ${validation.error}` : ''}`);
132
- return null;
133
- }
134
- }
135
- }
136
- let processedContent = content;
137
- let addedMintlifyTitle = false;
138
- if (fileType === 'mdx' &&
139
- settings.options?.mintlify?.inferTitleFromFilename) {
140
- const result = applyMintlifyTitleFallback(processedContent, relativePath, settings.defaultLocale);
141
- processedContent = result.content;
142
- addedMintlifyTitle = result.addedTitle;
124
+ const processed = preprocessContent(content, relativePath, fileType, settings);
125
+ if (typeof processed !== 'string') {
126
+ logger.warn(`Skipping ${relativePath}: ${processed.skip}`);
127
+ recordWarning('skipped_file', relativePath, processed.skip);
128
+ return null;
143
129
  }
144
- const sanitizedContent = sanitizeFileContent(processedContent);
145
- // Always hash original content for versionId
146
- const computedVersionId = hashStringSync(content);
147
130
  return {
148
- content: sanitizedContent,
131
+ content: processed,
149
132
  fileName: relativePath,
150
133
  fileFormat: fileType.toUpperCase(),
151
134
  fileId: hashStringSync(relativePath),
152
- versionId: computedVersionId,
135
+ versionId: hashStringSync(content),
153
136
  locale: settings.defaultLocale,
154
137
  };
155
138
  })
@@ -0,0 +1,6 @@
1
+ import { Settings } from '../../../types/index.js';
2
+ /**
3
+ * Runs MDX-specific preprocessing. Returns a skip reason if the file
4
+ * should be skipped, or null if validation passed.
5
+ */
6
+ export declare function preprocessMdx(content: string, filePath: string, settings: Settings): string | null;
@@ -0,0 +1,14 @@
1
+ import { isValidMdx } from '../../../utils/validateMdx.js';
2
+ /**
3
+ * Runs MDX-specific preprocessing. Returns a skip reason if the file
4
+ * should be skipped, or null if validation passed.
5
+ */
6
+ export function preprocessMdx(content, filePath, settings) {
7
+ if (!settings.options?.skipFileValidation?.mdx) {
8
+ const validation = isValidMdx(content, filePath);
9
+ if (!validation.isValid) {
10
+ return `MDX file is not AST parsable${validation.error ? `: ${validation.error}` : ''}`;
11
+ }
12
+ }
13
+ return null;
14
+ }
@@ -0,0 +1,5 @@
1
+ import { Settings } from '../../../types/index.js';
2
+ /**
3
+ * Runs all Mintlify-specific preprocessing on file content.
4
+ */
5
+ export declare function preprocessMintlify(content: string, filePath: string, fileType: string, settings: Settings): string;
@@ -0,0 +1,15 @@
1
+ import { applyMintlifyTitleFallback } from '../../../utils/mintlifyTitleFallback.js';
2
+ import wrapPlainUrls from '../../../utils/wrapPlainUrls.js';
3
+ /**
4
+ * Runs all Mintlify-specific preprocessing on file content.
5
+ */
6
+ export function preprocessMintlify(content, filePath, fileType, settings) {
7
+ if (fileType !== 'mdx')
8
+ return content;
9
+ let result = content;
10
+ if (settings.options?.mintlify?.inferTitleFromFilename) {
11
+ result = applyMintlifyTitleFallback(result, filePath, settings.defaultLocale).content;
12
+ }
13
+ result = wrapPlainUrls(result);
14
+ return result;
15
+ }
@@ -0,0 +1,8 @@
1
+ import { Settings } from '../../types/index.js';
2
+ /**
3
+ * Preprocesses file content before upload. Returns the processed content,
4
+ * or { skip: reason } if the file should be skipped.
5
+ */
6
+ export declare function preprocessContent(content: string, filePath: string, fileType: string, settings: Settings): string | {
7
+ skip: string;
8
+ };
@@ -0,0 +1,23 @@
1
+ import { preprocessMdx } from './preprocess/mdx.js';
2
+ import { preprocessMintlify } from './preprocess/mintlify.js';
3
+ import sanitizeFileContent from '../../utils/sanitizeFileContent.js';
4
+ /**
5
+ * Preprocesses file content before upload. Returns the processed content,
6
+ * or { skip: reason } if the file should be skipped.
7
+ */
8
+ export function preprocessContent(content, filePath, fileType, settings) {
9
+ let result = content;
10
+ // File-type-specific
11
+ if (fileType === 'mdx') {
12
+ const skipReason = preprocessMdx(result, filePath, settings);
13
+ if (skipReason)
14
+ return { skip: skipReason };
15
+ }
16
+ // Framework-specific
17
+ if (settings.framework === 'mintlify') {
18
+ result = preprocessMintlify(result, filePath, fileType, settings);
19
+ }
20
+ // Universal
21
+ result = sanitizeFileContent(result);
22
+ return result;
23
+ }
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "2.6.30-alpha.0";
1
+ export declare const PACKAGE_VERSION = "2.7.0";
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
- export const PACKAGE_VERSION = '2.6.30-alpha.0';
2
+ export const PACKAGE_VERSION = '2.7.0';
@@ -7,7 +7,9 @@ export declare const INLINE_MESSAGE_HOOK = "useMessages";
7
7
  export declare const INLINE_MESSAGE_HOOK_ASYNC = "getMessages";
8
8
  export declare const TRANSLATION_COMPONENT = "T";
9
9
  export declare const STATIC_COMPONENT = "Static";
10
+ export declare const BRANCH_COMPONENT = "Branch";
10
11
  export declare const GT_TRANSLATION_FUNCS: string[];
11
12
  export declare const VARIABLE_COMPONENTS: string[];
12
13
  export declare const GT_ATTRIBUTES_WITH_SUGAR: readonly ["$id", "$context", "$maxChars"];
13
14
  export declare const GT_ATTRIBUTES: readonly ["id", "context", "maxChars", "$id", "$context", "$maxChars"];
15
+ export declare const DATA_ATTR_PREFIX: "data-";
@@ -7,6 +7,7 @@ export const INLINE_MESSAGE_HOOK = 'useMessages';
7
7
  export const INLINE_MESSAGE_HOOK_ASYNC = 'getMessages';
8
8
  export const TRANSLATION_COMPONENT = 'T';
9
9
  export const STATIC_COMPONENT = 'Static';
10
+ export const BRANCH_COMPONENT = 'Branch';
10
11
  // GT translation functions
11
12
  export const GT_TRANSLATION_FUNCS = [
12
13
  INLINE_TRANSLATION_HOOK,
@@ -22,7 +23,7 @@ export const GT_TRANSLATION_FUNCS = [
22
23
  'DateTime',
23
24
  'Currency',
24
25
  'Num',
25
- 'Branch',
26
+ BRANCH_COMPONENT,
26
27
  'Plural',
27
28
  ];
28
29
  // Valid variable components
@@ -44,3 +45,5 @@ export const GT_ATTRIBUTES = [
44
45
  'maxChars',
45
46
  ...GT_ATTRIBUTES_WITH_SUGAR,
46
47
  ];
48
+ // Data attribute prefix injected by build tools
49
+ export const DATA_ATTR_PREFIX = 'data-';
@@ -1,6 +1,7 @@
1
1
  import { HTML_CONTENT_PROPS, } from 'generaltranslation/types';
2
2
  import { defaultVariableNames, getVariableName, minifyVariableType, } from '../../../utils/getVariableName.js';
3
3
  import { isAcceptedPluralForm } from 'generaltranslation/internal';
4
+ import { DATA_ATTR_PREFIX } from '../constants.js';
4
5
  /**
5
6
  * Construct the data-_gt prop
6
7
  * @param type - The type of the element
@@ -33,7 +34,9 @@ function constructGTProp(type, props, id) {
33
34
  }
34
35
  else if (type === 'Branch') {
35
36
  // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
36
- const { children, branch, ...branches } = props;
37
+ const { children, branch, ...allBranches } = props;
38
+ // Filter out data-* attributes injected by build tools
39
+ const branches = Object.fromEntries(Object.entries(allBranches).filter(([key]) => !key.startsWith(DATA_ATTR_PREFIX)));
37
40
  const resultBranches = Object.entries(branches).reduce((acc, [branchName, branch]) => {
38
41
  acc[branchName] = addGTIdentifierToSyntaxTree(branch, id);
39
42
  return acc;
@@ -6,10 +6,10 @@ import * as t from '@babel/types';
6
6
  import fs from 'node:fs';
7
7
  import { parse } from '@babel/parser';
8
8
  import addGTIdentifierToSyntaxTree from './addGTIdentifierToSyntaxTree.js';
9
- import { warnHasUnwrappedExpressionSync, warnNestedTComponent, warnFunctionNotFoundSync, warnMissingReturnSync, warnDuplicateFunctionDefinitionSync, warnInvalidStaticInitSync, warnRecursiveFunctionCallSync, } from '../../../../console/index.js';
9
+ import { warnHasUnwrappedExpressionSync, warnNestedTComponent, warnFunctionNotFoundSync, warnMissingReturnSync, warnDuplicateFunctionDefinitionSync, warnInvalidStaticInitSync, warnRecursiveFunctionCallSync, warnDataAttrOnBranch, } from '../../../../console/index.js';
10
10
  import { isAcceptedPluralForm } from 'generaltranslation/internal';
11
11
  import { isStaticExpression } from '../../evaluateJsx.js';
12
- import { STATIC_COMPONENT, TRANSLATION_COMPONENT, VARIABLE_COMPONENTS, } from '../constants.js';
12
+ import { DATA_ATTR_PREFIX, STATIC_COMPONENT, TRANSLATION_COMPONENT, VARIABLE_COMPONENTS, } from '../constants.js';
13
13
  import { HTML_CONTENT_PROPS } from 'generaltranslation/types';
14
14
  import { resolveImportPath } from '../resolveImportPath.js';
15
15
  import traverseModule from '@babel/traverse';
@@ -161,6 +161,10 @@ function buildJSXTree({ node, helperPath, scopeNode, insideT, inStatic, config,
161
161
  ? attr.name.name
162
162
  : attr.name.name.name;
163
163
  let attrValue = null;
164
+ if (elementIsBranch && attrName.startsWith(DATA_ATTR_PREFIX)) {
165
+ const location = `${attr.loc?.start?.line}:${attr.loc?.start?.column}`;
166
+ output.errors.push(warnDataAttrOnBranch(config.file, attrName, location));
167
+ }
164
168
  if (attr.value) {
165
169
  if (t.isStringLiteral(attr.value)) {
166
170
  attrValue = attr.value.value;
@@ -171,7 +175,9 @@ function buildJSXTree({ node, helperPath, scopeNode, insideT, inStatic, config,
171
175
  const isHtmlContentProp = Object.values(HTML_CONTENT_PROPS).includes(attrName);
172
176
  // If its a plural or branch prop
173
177
  if ((elementIsPlural && isAcceptedPluralForm(attrName)) ||
174
- (elementIsBranch && attrName !== 'branch')) {
178
+ (elementIsBranch &&
179
+ attrName !== 'branch' &&
180
+ !attrName.startsWith(DATA_ATTR_PREFIX))) {
175
181
  // Make sure that variable strings like {`I have ${count} book`} are invalid!
176
182
  if (t.isTemplateLiteral(attr.value.expression) &&
177
183
  !isStaticExpression(attr.value.expression, true).isStatic) {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Wraps plain URLs in markdown link syntax [url](url) so that
3
+ * translation pipelines preserve the URL separately from surrounding text.
4
+ *
5
+ * Uses remark AST parsing to identify URLs that appear in text nodes only.
6
+ *
7
+ */
8
+ export default function wrapPlainUrls(content: string): string;
@@ -0,0 +1,72 @@
1
+ import { unified } from 'unified';
2
+ import remarkParse from 'remark-parse';
3
+ import remarkMdx from 'remark-mdx';
4
+ import remarkFrontmatter from 'remark-frontmatter';
5
+ import { visit } from 'unist-util-visit';
6
+ /**
7
+ * Wraps plain URLs in markdown link syntax [url](url) so that
8
+ * translation pipelines preserve the URL separately from surrounding text.
9
+ *
10
+ * Uses remark AST parsing to identify URLs that appear in text nodes only.
11
+ *
12
+ */
13
+ export default function wrapPlainUrls(content) {
14
+ const URL_REGEX = /https?:\/\/[^\s<>\[\]]*[^\s<>\[\].,;:!?'"\]}>]/g;
15
+ let ast;
16
+ try {
17
+ const processor = unified()
18
+ .use(remarkParse)
19
+ .use(remarkFrontmatter, ['yaml', 'toml'])
20
+ .use(remarkMdx);
21
+ ast = processor.parse(content);
22
+ ast = processor.runSync(ast);
23
+ }
24
+ catch {
25
+ // If parsing fails, return content unchanged
26
+ return content;
27
+ }
28
+ // Collect all URL replacements from text nodes with their positions
29
+ const replacements = [];
30
+ visit(ast, 'text', (node, _index, parent) => {
31
+ // Skip text nodes inside links — those are already display text for a link
32
+ if (parent && parent.type === 'link')
33
+ return;
34
+ const pos = node.position;
35
+ if (!pos)
36
+ return;
37
+ const value = node.value;
38
+ let match;
39
+ while ((match = URL_REGEX.exec(value)) !== null) {
40
+ let url = match[0];
41
+ const nodeStartOffset = pos.start.offset;
42
+ if (nodeStartOffset === undefined)
43
+ continue;
44
+ // Trim unbalanced trailing ')' so that prose like "(see https://example.com)"
45
+ // doesn't absorb the surrounding paren, while Wikipedia-style URLs with
46
+ // balanced parens (e.g. /wiki/Unix_(operating_system)) are kept intact.
47
+ while (url.endsWith(')')) {
48
+ const open = url.split('(').length - 1;
49
+ const close = url.split(')').length - 1;
50
+ if (close > open) {
51
+ url = url.slice(0, -1);
52
+ }
53
+ else {
54
+ break;
55
+ }
56
+ }
57
+ // Calculate the absolute offset in the original content
58
+ const urlStart = nodeStartOffset + match.index;
59
+ const urlEnd = urlStart + url.length;
60
+ replacements.push({ start: urlStart, end: urlEnd, url });
61
+ }
62
+ });
63
+ if (replacements.length === 0)
64
+ return content;
65
+ // Apply replacements in reverse order to preserve positions
66
+ let result = content;
67
+ for (let i = replacements.length - 1; i >= 0; i--) {
68
+ const { start, end, url } = replacements[i];
69
+ result = result.slice(0, start) + `[${url}](${url})` + result.slice(end);
70
+ }
71
+ return result;
72
+ }
@@ -75,6 +75,10 @@ export async function runDownloadWorkflow({ fileVersionData, jobData, branchData
75
75
  for (const [, value] of pollResult.fileTracker.failed) {
76
76
  recordWarning('failed_translation', value.fileName, `Failed to translate for locale ${value.locale}`);
77
77
  }
78
+ // If all files failed translation, exit early
79
+ if (pollResult.fileTracker.completed.size === 0) {
80
+ return false;
81
+ }
78
82
  }
79
83
  // Even if polling timed out, still download whatever completed successfully
80
84
  if (!pollResult.success) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gt",
3
- "version": "2.6.30-alpha.0",
3
+ "version": "2.7.0",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [
@@ -131,7 +131,8 @@
131
131
  "prettier": "^3.4.2",
132
132
  "ts-node": "^10.9.2",
133
133
  "tslib": "^2.8.1",
134
- "typescript": "^5.5.4"
134
+ "typescript": "^5.5.4",
135
+ "vitest": "^2.0.0"
135
136
  },
136
137
  "scripts": {
137
138
  "build": "node scripts/generate-version.js && tsc && rm -rf dist/setup/instructions && cp -r src/setup/instructions dist/setup/instructions",