gt 2.9.0 → 2.10.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 +12 -0
- package/dist/cli/base.js +1 -4
- package/dist/extraction/postProcess.js +14 -0
- package/dist/formats/files/aggregateFiles.js +80 -0
- package/dist/formats/json/parseJson.d.ts +1 -1
- package/dist/formats/json/parseJson.js +6 -4
- package/dist/formats/parseKeyedMetadata.d.ts +23 -0
- package/dist/formats/parseKeyedMetadata.js +111 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/react/jsx/utils/extractSourceCode.d.ts +15 -0
- package/dist/react/jsx/utils/extractSourceCode.js +39 -0
- package/dist/react/jsx/utils/jsxParsing/parseJsx.d.ts +1 -0
- package/dist/react/jsx/utils/jsxParsing/parseJsx.js +11 -0
- package/dist/react/jsx/utils/parseStringFunction.js +2 -0
- package/dist/react/jsx/utils/stringParsing/processTranslationCall/extractStringEntryMetadata.d.ts +12 -1
- package/dist/react/jsx/utils/stringParsing/processTranslationCall/extractStringEntryMetadata.js +14 -1
- package/dist/react/jsx/utils/stringParsing/processTranslationCall/index.js +3 -0
- package/dist/react/jsx/utils/stringParsing/types.d.ts +4 -0
- package/dist/react/parse/createInlineUpdates.d.ts +1 -1
- package/dist/react/parse/createInlineUpdates.js +3 -1
- package/dist/translation/parse.d.ts +1 -1
- package/dist/translation/parse.js +2 -2
- package/dist/translation/stage.js +1 -1
- package/dist/translation/validate.js +2 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/constants.d.ts +1 -0
- package/dist/utils/constants.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# gtx-cli
|
|
2
2
|
|
|
3
|
+
## 2.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#1104](https://github.com/generaltranslation/gt/pull/1104) [`51430bd`](https://github.com/generaltranslation/gt/commit/51430bd1d85a4937ff3b4dcd0090d79e3b4c1504) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Adding metadata support for keyed file types
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#1101](https://github.com/generaltranslation/gt/pull/1101) [`437a389`](https://github.com/generaltranslation/gt/commit/437a3898f1daa0a40ac033c2cc1bb94b4a0fd86b) Thanks [@ErnestM1234](https://github.com/ErnestM1234)! - fix: remove tw content json from init
|
|
12
|
+
|
|
13
|
+
- [#1103](https://github.com/generaltranslation/gt/pull/1103) [`7164ceb`](https://github.com/generaltranslation/gt/commit/7164ceb9785863cdf4dc659fe5bd0f87511a5bed) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Extract code metadata
|
|
14
|
+
|
|
3
15
|
## 2.9.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/dist/cli/base.js
CHANGED
|
@@ -399,10 +399,7 @@ See https://generaltranslation.com/en/docs/next/guides/local-tx`);
|
|
|
399
399
|
{ value: 'ts', label: FILE_EXT_TO_EXT_LABEL.ts },
|
|
400
400
|
{ value: 'js', label: FILE_EXT_TO_EXT_LABEL.js },
|
|
401
401
|
{ value: 'yaml', label: FILE_EXT_TO_EXT_LABEL.yaml },
|
|
402
|
-
|
|
403
|
-
value: 'twilioContentJson',
|
|
404
|
-
label: FILE_EXT_TO_EXT_LABEL.twilioContentJson,
|
|
405
|
-
},
|
|
402
|
+
// TWILIO_CONTENT_JSON not supported in CLI init as its too niche
|
|
406
403
|
],
|
|
407
404
|
required: !isUsingGT,
|
|
408
405
|
});
|
|
@@ -47,6 +47,20 @@ export function dedupeUpdates(updates) {
|
|
|
47
47
|
if (existingPaths.length) {
|
|
48
48
|
existing.metadata.filePaths = existingPaths;
|
|
49
49
|
}
|
|
50
|
+
// Merge sourceCode entries
|
|
51
|
+
const newSourceCode = update.metadata.sourceCode;
|
|
52
|
+
if (newSourceCode && typeof newSourceCode === 'object') {
|
|
53
|
+
if (!existing.metadata.sourceCode) {
|
|
54
|
+
existing.metadata.sourceCode = {};
|
|
55
|
+
}
|
|
56
|
+
const existingSourceCode = existing.metadata.sourceCode;
|
|
57
|
+
for (const [file, entries] of Object.entries(newSourceCode)) {
|
|
58
|
+
if (!existingSourceCode[file]) {
|
|
59
|
+
existingSourceCode[file] = [];
|
|
60
|
+
}
|
|
61
|
+
existingSourceCode[file].push(...entries);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
50
64
|
}
|
|
51
65
|
const mergedUpdates = [...mergedByHash.values(), ...noHashUpdates];
|
|
52
66
|
updates.splice(0, updates.length, ...mergedUpdates);
|
|
@@ -4,10 +4,26 @@ import { getRelative, readFile } from '../../fs/findFilepath.js';
|
|
|
4
4
|
import { SUPPORTED_FILE_EXTENSIONS } from './supportedFiles.js';
|
|
5
5
|
import { parseJson } from '../json/parseJson.js';
|
|
6
6
|
import parseYaml from '../yaml/parseYaml.js';
|
|
7
|
+
import { validateYamlSchema } from '../yaml/utils.js';
|
|
8
|
+
import { flattenJson } from '../json/flattenJson.js';
|
|
7
9
|
import YAML from 'yaml';
|
|
8
10
|
import { determineLibrary } from '../../fs/determineFramework/index.js';
|
|
9
11
|
import { hashStringSync } from '../../utils/hash.js';
|
|
10
12
|
import { preprocessContent } from './preprocessContent.js';
|
|
13
|
+
import { parseKeyedMetadata, } from '../parseKeyedMetadata.js';
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a file path is a metadata companion file (e.g. foo.metadata.json)
|
|
16
|
+
* AND its corresponding source file (e.g. foo.json) exists in the file list.
|
|
17
|
+
* If both conditions are true, the metadata file should be skipped as a translation source.
|
|
18
|
+
*/
|
|
19
|
+
function isCompanionMetadataFile(filePath, allFilePaths) {
|
|
20
|
+
const metadataPattern = /\.metadata\.(json|yaml|yml)$/;
|
|
21
|
+
if (!metadataPattern.test(filePath))
|
|
22
|
+
return false;
|
|
23
|
+
// Derive the source file path: foo.metadata.json -> foo.json
|
|
24
|
+
const sourceFilePath = filePath.replace(/\.metadata\.(json|yaml|yml)$/, '.$1');
|
|
25
|
+
return allFilePaths.includes(sourceFilePath);
|
|
26
|
+
}
|
|
11
27
|
export const SUPPORTED_DATA_FORMATS = ['JSX', 'ICU', 'I18NEXT'];
|
|
12
28
|
export async function aggregateFiles(settings) {
|
|
13
29
|
// Aggregate all files to translate
|
|
@@ -39,6 +55,7 @@ export async function aggregateFiles(settings) {
|
|
|
39
55
|
dataFormat = 'STRING';
|
|
40
56
|
}
|
|
41
57
|
const jsonFiles = filePaths.json
|
|
58
|
+
.filter((filePath) => !isCompanionMetadataFile(filePath, filePaths.json))
|
|
42
59
|
.map((filePath) => {
|
|
43
60
|
const content = readFile(filePath);
|
|
44
61
|
const relativePath = getRelative(filePath);
|
|
@@ -54,6 +71,34 @@ export async function aggregateFiles(settings) {
|
|
|
54
71
|
}
|
|
55
72
|
}
|
|
56
73
|
const parsedJson = parseJson(content, filePath, settings.options || {}, settings.defaultLocale);
|
|
74
|
+
// Detect companion metadata file
|
|
75
|
+
let keyedMetadata;
|
|
76
|
+
let parsedContent;
|
|
77
|
+
try {
|
|
78
|
+
parsedContent = JSON.parse(content);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Content not parsable — skip metadata detection
|
|
82
|
+
}
|
|
83
|
+
if (parsedContent) {
|
|
84
|
+
const rawMetadata = parseKeyedMetadata(filePath, parsedContent);
|
|
85
|
+
if (rawMetadata) {
|
|
86
|
+
// Run metadata through the same include/composite schema as the source
|
|
87
|
+
// so key paths align at translation time
|
|
88
|
+
const transformed = parseJson(JSON.stringify(rawMetadata), filePath, settings.options || {}, settings.defaultLocale, false);
|
|
89
|
+
const transformedMetadata = JSON.parse(transformed);
|
|
90
|
+
// Filter metadata to only keep keys that exist in the transformed source
|
|
91
|
+
// This prevents misaligned entries from wide JSONPath patterns
|
|
92
|
+
const sourceKeys = new Set(Object.keys(JSON.parse(parsedJson)));
|
|
93
|
+
const filtered = Object.fromEntries(Object.entries(transformedMetadata).filter(([k]) => sourceKeys.has(k)));
|
|
94
|
+
if (Object.keys(filtered).length > 0) {
|
|
95
|
+
keyedMetadata = filtered;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
logger.warn(`Companion metadata found for ${relativePath} but no keys aligned with the JSON schema — metadata was not attached`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
57
102
|
return {
|
|
58
103
|
fileId: hashStringSync(relativePath),
|
|
59
104
|
versionId: hashStringSync(parsedJson),
|
|
@@ -62,6 +107,9 @@ export async function aggregateFiles(settings) {
|
|
|
62
107
|
fileFormat: 'JSON',
|
|
63
108
|
dataFormat,
|
|
64
109
|
locale: settings.defaultLocale,
|
|
110
|
+
...(keyedMetadata && {
|
|
111
|
+
formatMetadata: { keyedMetadata },
|
|
112
|
+
}),
|
|
65
113
|
};
|
|
66
114
|
})
|
|
67
115
|
.filter((file) => {
|
|
@@ -79,6 +127,7 @@ export async function aggregateFiles(settings) {
|
|
|
79
127
|
// Process YAML files
|
|
80
128
|
if (filePaths.yaml) {
|
|
81
129
|
const yamlFiles = filePaths.yaml
|
|
130
|
+
.filter((filePath) => !isCompanionMetadataFile(filePath, filePaths.yaml))
|
|
82
131
|
.map((filePath) => {
|
|
83
132
|
const content = readFile(filePath);
|
|
84
133
|
const relativePath = getRelative(filePath);
|
|
@@ -94,6 +143,34 @@ export async function aggregateFiles(settings) {
|
|
|
94
143
|
}
|
|
95
144
|
}
|
|
96
145
|
const { content: parsedYaml, fileFormat } = parseYaml(content, filePath, settings.options || {});
|
|
146
|
+
// Detect companion metadata file
|
|
147
|
+
let keyedMetadata;
|
|
148
|
+
try {
|
|
149
|
+
const parsedYamlContent = YAML.parse(content);
|
|
150
|
+
const rawMetadata = parseKeyedMetadata(filePath, parsedYamlContent);
|
|
151
|
+
if (rawMetadata) {
|
|
152
|
+
const yamlSchema = validateYamlSchema(settings.options || {}, filePath);
|
|
153
|
+
if (yamlSchema?.include) {
|
|
154
|
+
// Flatten metadata through the same include schema as the source
|
|
155
|
+
const flattened = flattenJson(rawMetadata, yamlSchema.include);
|
|
156
|
+
// Filter to only keep keys that exist in the transformed source
|
|
157
|
+
const sourceKeys = new Set(Object.keys(JSON.parse(parsedYaml)));
|
|
158
|
+
const filtered = Object.fromEntries(Object.entries(flattened).filter(([k]) => sourceKeys.has(k)));
|
|
159
|
+
if (Object.keys(filtered).length > 0) {
|
|
160
|
+
keyedMetadata = filtered;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
logger.warn(`Companion metadata found for ${relativePath} but no keys aligned with the YAML schema — metadata was not attached`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
keyedMetadata = rawMetadata;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Content not parsable as YAML — skip metadata detection
|
|
173
|
+
}
|
|
97
174
|
return {
|
|
98
175
|
content: parsedYaml,
|
|
99
176
|
fileName: relativePath,
|
|
@@ -101,6 +178,9 @@ export async function aggregateFiles(settings) {
|
|
|
101
178
|
fileId: hashStringSync(relativePath),
|
|
102
179
|
versionId: hashStringSync(parsedYaml),
|
|
103
180
|
locale: settings.defaultLocale,
|
|
181
|
+
...(keyedMetadata && {
|
|
182
|
+
formatMetadata: { keyedMetadata },
|
|
183
|
+
}),
|
|
104
184
|
};
|
|
105
185
|
})
|
|
106
186
|
.filter((file) => {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { AdditionalOptions } from '../../types/index.js';
|
|
2
|
-
export declare function parseJson(content: string, filePath: string, options: AdditionalOptions, defaultLocale: string): string;
|
|
2
|
+
export declare function parseJson(content: string, filePath: string, options: AdditionalOptions, defaultLocale: string, filterStrings?: boolean): string;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { flattenJsonWithStringFilter } from './flattenJson.js';
|
|
1
|
+
import { flattenJson, flattenJsonWithStringFilter } from './flattenJson.js';
|
|
2
2
|
import { JSONPath } from 'jsonpath-plus';
|
|
3
3
|
import { exitSync } from '../../console/logging.js';
|
|
4
4
|
import { logger } from '../../console/logger.js';
|
|
5
5
|
import { findMatchingItemArray, findMatchingItemObject, generateSourceObjectPointers, validateJsonSchema, } from './utils.js';
|
|
6
6
|
import { applyStructuralTransforms } from './transformJson.js';
|
|
7
7
|
// Parse a JSON file according to a JSON schema
|
|
8
|
-
export function parseJson(content, filePath, options, defaultLocale) {
|
|
8
|
+
export function parseJson(content, filePath, options, defaultLocale, filterStrings = true) {
|
|
9
9
|
const jsonSchema = validateJsonSchema(options, filePath);
|
|
10
10
|
if (!jsonSchema) {
|
|
11
11
|
return content;
|
|
@@ -23,7 +23,8 @@ export function parseJson(content, filePath, options, defaultLocale) {
|
|
|
23
23
|
}
|
|
24
24
|
// Handle include
|
|
25
25
|
if (jsonSchema.include) {
|
|
26
|
-
const
|
|
26
|
+
const flatten = filterStrings ? flattenJsonWithStringFilter : flattenJson;
|
|
27
|
+
const flattenedJson = flatten(json, jsonSchema.include);
|
|
27
28
|
return JSON.stringify(flattenedJson);
|
|
28
29
|
}
|
|
29
30
|
if (!jsonSchema.composite) {
|
|
@@ -104,7 +105,8 @@ export function parseJson(content, filePath, options, defaultLocale) {
|
|
|
104
105
|
}
|
|
105
106
|
const { sourceItem } = matchingItem;
|
|
106
107
|
// Get the fields to translate from the includes
|
|
107
|
-
const
|
|
108
|
+
const flatten = filterStrings ? flattenJsonWithStringFilter : flattenJson;
|
|
109
|
+
const itemsToTranslate = flatten(sourceItem, sourceObjectOptions.include);
|
|
108
110
|
// Add the items to translate to the result
|
|
109
111
|
sourceObjectsToTranslate[sourceObjectPointer] = itemsToTranslate;
|
|
110
112
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SourceCode } from '../react/jsx/utils/extractSourceCode.js';
|
|
2
|
+
import type { JSONObject } from '../types/data/json.js';
|
|
3
|
+
export type MetadataLeaf = {
|
|
4
|
+
context?: string;
|
|
5
|
+
maxChars?: number;
|
|
6
|
+
sourceCode?: Record<string, SourceCode[]>;
|
|
7
|
+
};
|
|
8
|
+
export type MetadataObject = {
|
|
9
|
+
[key: string]: MetadataLeaf | MetadataObject;
|
|
10
|
+
};
|
|
11
|
+
export type MetadataArray = (MetadataLeaf | MetadataObject)[];
|
|
12
|
+
export type KeyedMetadata = MetadataObject | MetadataArray;
|
|
13
|
+
/**
|
|
14
|
+
* Detects and parses a companion metadata file for a given source file.
|
|
15
|
+
*
|
|
16
|
+
* For `translations.json`, looks for `translations.metadata.json`.
|
|
17
|
+
* For `translations.yaml` or `translations.yml`, looks for `translations.metadata.yaml` or `.yml`.
|
|
18
|
+
*
|
|
19
|
+
* @param sourceFilePath - Absolute path to the source file
|
|
20
|
+
* @param sourceContent - Parsed source content (object) for structure validation
|
|
21
|
+
* @returns Parsed metadata object, or undefined if no companion file exists
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseKeyedMetadata(sourceFilePath: string, sourceContent: JSONObject | JSONObject[]): KeyedMetadata | undefined;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { logger } from '../console/logger.js';
|
|
5
|
+
import { exitSync } from '../console/logging.js';
|
|
6
|
+
/**
|
|
7
|
+
* Validates that the metadata key structure is a subset of the source key structure.
|
|
8
|
+
* Uses the source to determine whether a metadata value is a leaf (source value is a string)
|
|
9
|
+
* or a nested object (source value is an object).
|
|
10
|
+
*/
|
|
11
|
+
function validateMetadataStructure(source, metadata, currentPath = []) {
|
|
12
|
+
const errors = [];
|
|
13
|
+
for (const key of Object.keys(metadata)) {
|
|
14
|
+
const sourceValue = source[key];
|
|
15
|
+
const keyPath = [...currentPath, key];
|
|
16
|
+
if (sourceValue === undefined) {
|
|
17
|
+
errors.push(`Key "${keyPath.join('.')}" does not exist in source`);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
// If the source value is a string, this is a translatable leaf — metadata should be a MetadataLeaf
|
|
21
|
+
// If the source value is a nested object, recurse
|
|
22
|
+
if (typeof sourceValue === 'object' &&
|
|
23
|
+
sourceValue !== null &&
|
|
24
|
+
!Array.isArray(sourceValue)) {
|
|
25
|
+
const metaValue = metadata[key];
|
|
26
|
+
if (Array.isArray(metaValue)) {
|
|
27
|
+
errors.push(`Key "${keyPath.join('.')}" is an array but source is an object`);
|
|
28
|
+
}
|
|
29
|
+
else if (typeof metaValue === 'object' && metaValue !== null) {
|
|
30
|
+
errors.push(...validateMetadataStructure(sourceValue, metaValue, keyPath));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
errors.push(`Key "${keyPath.join('.')}" is a primitive but source is an object`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return errors;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Detects and parses a companion metadata file for a given source file.
|
|
41
|
+
*
|
|
42
|
+
* For `translations.json`, looks for `translations.metadata.json`.
|
|
43
|
+
* For `translations.yaml` or `translations.yml`, looks for `translations.metadata.yaml` or `.yml`.
|
|
44
|
+
*
|
|
45
|
+
* @param sourceFilePath - Absolute path to the source file
|
|
46
|
+
* @param sourceContent - Parsed source content (object) for structure validation
|
|
47
|
+
* @returns Parsed metadata object, or undefined if no companion file exists
|
|
48
|
+
*/
|
|
49
|
+
export function parseKeyedMetadata(sourceFilePath, sourceContent) {
|
|
50
|
+
const ext = path.extname(sourceFilePath);
|
|
51
|
+
const baseName = sourceFilePath.slice(0, -ext.length);
|
|
52
|
+
// Determine companion file path and parser
|
|
53
|
+
let metadataFilePath;
|
|
54
|
+
let parse;
|
|
55
|
+
if (ext === '.json') {
|
|
56
|
+
metadataFilePath = `${baseName}.metadata.json`;
|
|
57
|
+
parse = JSON.parse;
|
|
58
|
+
}
|
|
59
|
+
else if (ext === '.yaml' || ext === '.yml') {
|
|
60
|
+
const yamlPath = `${baseName}.metadata.yaml`;
|
|
61
|
+
const ymlPath = `${baseName}.metadata.yml`;
|
|
62
|
+
if (fs.existsSync(yamlPath)) {
|
|
63
|
+
metadataFilePath = yamlPath;
|
|
64
|
+
}
|
|
65
|
+
else if (fs.existsSync(ymlPath)) {
|
|
66
|
+
metadataFilePath = ymlPath;
|
|
67
|
+
}
|
|
68
|
+
parse = YAML.parse;
|
|
69
|
+
}
|
|
70
|
+
if (!metadataFilePath || !parse) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
if (!fs.existsSync(metadataFilePath)) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
// Read and parse
|
|
77
|
+
let metadataContent;
|
|
78
|
+
try {
|
|
79
|
+
const raw = fs.readFileSync(metadataFilePath, 'utf8');
|
|
80
|
+
const parsed = parse(raw);
|
|
81
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
82
|
+
const relativePath = path.relative(process.cwd(), metadataFilePath);
|
|
83
|
+
logger.error(`Metadata file ${relativePath}: Expected an object or array`);
|
|
84
|
+
return exitSync(1);
|
|
85
|
+
}
|
|
86
|
+
metadataContent = parsed;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
const relativePath = path.relative(process.cwd(), metadataFilePath);
|
|
90
|
+
logger.error(`Metadata file ${relativePath}: File is not parsable`);
|
|
91
|
+
return exitSync(1);
|
|
92
|
+
}
|
|
93
|
+
// Reject if root types don't match (array vs object)
|
|
94
|
+
if (Array.isArray(metadataContent) !== Array.isArray(sourceContent)) {
|
|
95
|
+
const relativePath = path.relative(process.cwd(), metadataFilePath);
|
|
96
|
+
logger.error(`Metadata file ${relativePath}: Root type (array vs object) does not match source`);
|
|
97
|
+
return exitSync(1);
|
|
98
|
+
}
|
|
99
|
+
// Validate structure against source (only for object-rooted files)
|
|
100
|
+
if (!Array.isArray(metadataContent) && !Array.isArray(sourceContent)) {
|
|
101
|
+
const errors = validateMetadataStructure(sourceContent, metadataContent);
|
|
102
|
+
if (errors.length > 0) {
|
|
103
|
+
const relativePath = path.relative(process.cwd(), metadataFilePath);
|
|
104
|
+
for (const error of errors) {
|
|
105
|
+
logger.error(`Metadata file ${relativePath}: ${error}`);
|
|
106
|
+
}
|
|
107
|
+
return exitSync(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return metadataContent;
|
|
111
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "2.
|
|
1
|
+
export declare const PACKAGE_VERSION = "2.10.0";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// This file is auto-generated. Do not edit manually.
|
|
2
|
-
export const PACKAGE_VERSION = '2.
|
|
2
|
+
export const PACKAGE_VERSION = '2.10.0';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type SourceCode = {
|
|
2
|
+
before: string;
|
|
3
|
+
target: string;
|
|
4
|
+
after: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Extracts the surrounding lines of source code around a target node.
|
|
8
|
+
*
|
|
9
|
+
* @param filePath - Absolute path to the source file
|
|
10
|
+
* @param startLine - 1-based start line of the target node
|
|
11
|
+
* @param endLine - 1-based end line of the target node
|
|
12
|
+
* @param n - Number of surrounding lines before and after to capture
|
|
13
|
+
* @returns The surrounding lines, or undefined if the file can't be read
|
|
14
|
+
*/
|
|
15
|
+
export declare function extractSourceCode(filePath: string, startLine: number, endLine: number, n: number): SourceCode | undefined;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
// Cache file contents to avoid re-reading the same file for multiple translation sites
|
|
3
|
+
const fileContentCache = new Map();
|
|
4
|
+
/**
|
|
5
|
+
* Extracts the surrounding lines of source code around a target node.
|
|
6
|
+
*
|
|
7
|
+
* @param filePath - Absolute path to the source file
|
|
8
|
+
* @param startLine - 1-based start line of the target node
|
|
9
|
+
* @param endLine - 1-based end line of the target node
|
|
10
|
+
* @param n - Number of surrounding lines before and after to capture
|
|
11
|
+
* @returns The surrounding lines, or undefined if the file can't be read
|
|
12
|
+
*/
|
|
13
|
+
export function extractSourceCode(filePath, startLine, endLine, n) {
|
|
14
|
+
let fileContent = fileContentCache.get(filePath);
|
|
15
|
+
if (fileContent === undefined) {
|
|
16
|
+
try {
|
|
17
|
+
const result = fs.readFileSync(filePath, 'utf8');
|
|
18
|
+
if (typeof result !== 'string') {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
fileContent = result;
|
|
22
|
+
fileContentCache.set(filePath, fileContent);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const lines = fileContent.split('\n');
|
|
29
|
+
const totalLines = lines.length;
|
|
30
|
+
// Clamp to valid line ranges (convert to 0-based)
|
|
31
|
+
const targetStart = Math.max(0, startLine - 1);
|
|
32
|
+
const targetEnd = Math.min(totalLines - 1, endLine - 1);
|
|
33
|
+
const beforeStart = Math.max(0, targetStart - n);
|
|
34
|
+
const afterEnd = Math.min(totalLines - 1, targetEnd + n);
|
|
35
|
+
const before = lines.slice(beforeStart, targetStart).join('\n');
|
|
36
|
+
const target = lines.slice(targetStart, targetEnd + 1).join('\n');
|
|
37
|
+
const after = lines.slice(targetEnd + 1, afterEnd + 1).join('\n');
|
|
38
|
+
return { before, target, after };
|
|
39
|
+
}
|
|
@@ -21,6 +21,8 @@ import { isElementNode } from './types.js';
|
|
|
21
21
|
import { multiplyJsxTree } from './multiplication/multiplyJsxTree.js';
|
|
22
22
|
import { removeNullChildrenFields } from './removeNullChildrenFields.js';
|
|
23
23
|
import path from 'node:path';
|
|
24
|
+
import { extractSourceCode } from '../extractSourceCode.js';
|
|
25
|
+
import { SURROUNDING_LINE_COUNT } from '../../../../utils/constants.js';
|
|
24
26
|
// Handle CommonJS/ESM interop
|
|
25
27
|
const traverse = traverseModule.default || traverseModule;
|
|
26
28
|
// TODO: currently we cover VariableDeclaration and FunctionDeclaration nodes, but are there others we should cover as well?
|
|
@@ -410,6 +412,15 @@ function parseJSXElement({ node, originalName, scopeNode, updates, config, state
|
|
|
410
412
|
const metadata = {};
|
|
411
413
|
const relativeFilepath = path.relative(process.cwd(), config.file);
|
|
412
414
|
metadata.filePaths = [relativeFilepath];
|
|
415
|
+
// Extract surrounding lines from source file
|
|
416
|
+
const startLine = node.loc?.start?.line;
|
|
417
|
+
const endLine = node.loc?.end?.line;
|
|
418
|
+
if (config.includeSourceCodeContext && startLine && endLine) {
|
|
419
|
+
const entry = extractSourceCode(config.file, startLine, endLine, SURROUNDING_LINE_COUNT);
|
|
420
|
+
if (entry && relativeFilepath) {
|
|
421
|
+
metadata.sourceCode = { [relativeFilepath]: [entry] };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
413
424
|
// We'll track this flag to know if any unwrapped {variable} is found in children
|
|
414
425
|
const unwrappedExpressions = [];
|
|
415
426
|
// Gather <T>'s props
|
|
@@ -283,6 +283,7 @@ export function parseStrings(importName, originalName, path, config, output) {
|
|
|
283
283
|
ignoreDynamicContent: false,
|
|
284
284
|
ignoreInvalidIcu: false,
|
|
285
285
|
ignoreInlineListContent: false,
|
|
286
|
+
includeSourceCodeContext: config.includeSourceCodeContext,
|
|
286
287
|
};
|
|
287
288
|
// Check if this is a direct call to msg('string')
|
|
288
289
|
if (refPath.parent.type === 'CallExpression' &&
|
|
@@ -322,6 +323,7 @@ export function parseStrings(importName, originalName, path, config, output) {
|
|
|
322
323
|
ignoreInvalidIcu: isMessageHook,
|
|
323
324
|
// TODO: when we add support for array content in gt function, this should just always be false
|
|
324
325
|
ignoreInlineListContent: isInlineGT,
|
|
326
|
+
includeSourceCodeContext: config.includeSourceCodeContext,
|
|
325
327
|
};
|
|
326
328
|
const effectiveParent = parentPath?.node.type === 'AwaitExpression'
|
|
327
329
|
? parentPath.parentPath
|
package/dist/react/jsx/utils/stringParsing/processTranslationCall/extractStringEntryMetadata.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as t from '@babel/types';
|
|
2
2
|
import { ParsingConfig } from '../types.js';
|
|
3
3
|
import { ParsingOutput } from '../types.js';
|
|
4
|
+
import type { SourceCode } from '../../extractSourceCode.js';
|
|
4
5
|
/**
|
|
5
6
|
* Metadata record type
|
|
6
7
|
*/
|
|
@@ -10,6 +11,7 @@ export type InlineMetadata = {
|
|
|
10
11
|
id?: string;
|
|
11
12
|
hash?: string;
|
|
12
13
|
filePaths?: string[];
|
|
14
|
+
sourceCode?: Record<string, SourceCode[]>;
|
|
13
15
|
};
|
|
14
16
|
/**
|
|
15
17
|
* Extracts inline metadata from a string entry
|
|
@@ -22,8 +24,17 @@ export type InlineMetadata = {
|
|
|
22
24
|
* @note - this function does not automatically append the index to the id, this must be done manually in the caller.
|
|
23
25
|
*
|
|
24
26
|
*/
|
|
25
|
-
export declare function extractStringEntryMetadata({ options, output, config, }: {
|
|
27
|
+
export declare function extractStringEntryMetadata({ options, output, config, nodeLoc, surroundingLineCount, }: {
|
|
26
28
|
options?: t.CallExpression['arguments'][number];
|
|
27
29
|
output: ParsingOutput;
|
|
28
30
|
config: ParsingConfig;
|
|
31
|
+
nodeLoc?: {
|
|
32
|
+
start?: {
|
|
33
|
+
line: number;
|
|
34
|
+
} | null;
|
|
35
|
+
end?: {
|
|
36
|
+
line: number;
|
|
37
|
+
} | null;
|
|
38
|
+
} | null;
|
|
39
|
+
surroundingLineCount?: number;
|
|
29
40
|
}): InlineMetadata;
|
package/dist/react/jsx/utils/stringParsing/processTranslationCall/extractStringEntryMetadata.js
CHANGED
|
@@ -7,6 +7,8 @@ import generateModule from '@babel/generator';
|
|
|
7
7
|
import { mapAttributeName } from '../../mapAttributeName.js';
|
|
8
8
|
import pathModule from 'node:path';
|
|
9
9
|
import { isNumberLiteral } from '../../isNumberLiteral.js';
|
|
10
|
+
import { extractSourceCode } from '../../extractSourceCode.js';
|
|
11
|
+
import { SURROUNDING_LINE_COUNT } from '../../../../../utils/constants.js';
|
|
10
12
|
// Handle CommonJS/ESM interop
|
|
11
13
|
const generate = generateModule.default || generateModule;
|
|
12
14
|
/**
|
|
@@ -20,7 +22,7 @@ const generate = generateModule.default || generateModule;
|
|
|
20
22
|
* @note - this function does not automatically append the index to the id, this must be done manually in the caller.
|
|
21
23
|
*
|
|
22
24
|
*/
|
|
23
|
-
export function extractStringEntryMetadata({ options, output, config, }) {
|
|
25
|
+
export function extractStringEntryMetadata({ options, output, config, nodeLoc, surroundingLineCount = SURROUNDING_LINE_COUNT, }) {
|
|
24
26
|
// extract filepath for entry
|
|
25
27
|
const relativeFilepath = pathModule.relative(process.cwd(), config.file);
|
|
26
28
|
// extract inline metadata
|
|
@@ -29,9 +31,20 @@ export function extractStringEntryMetadata({ options, output, config, }) {
|
|
|
29
31
|
output,
|
|
30
32
|
config,
|
|
31
33
|
});
|
|
34
|
+
// extract surrounding lines from source file
|
|
35
|
+
let sourceCode;
|
|
36
|
+
if (config.includeSourceCodeContext &&
|
|
37
|
+
nodeLoc?.start?.line &&
|
|
38
|
+
nodeLoc?.end?.line) {
|
|
39
|
+
const entry = extractSourceCode(config.file, nodeLoc.start.line, nodeLoc.end.line, surroundingLineCount);
|
|
40
|
+
if (entry && relativeFilepath) {
|
|
41
|
+
sourceCode = { [relativeFilepath]: [entry] };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
32
44
|
return {
|
|
33
45
|
...inlineMetadata,
|
|
34
46
|
filePaths: relativeFilepath ? [relativeFilepath] : undefined,
|
|
47
|
+
...(sourceCode && { sourceCode }),
|
|
35
48
|
};
|
|
36
49
|
}
|
|
37
50
|
// ----- Helper Functions ----- //
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { routeTranslationCall } from './routeTranslationCall.js';
|
|
2
2
|
import { extractStringEntryMetadata } from './extractStringEntryMetadata.js';
|
|
3
|
+
import { SURROUNDING_LINE_COUNT } from '../../../../../utils/constants.js';
|
|
3
4
|
/**
|
|
4
5
|
* Processes a single translation function call (e.g., t('hello world', { id: 'greeting' })).
|
|
5
6
|
* Extracts the translatable string content and metadata, then adds it to the updates array.
|
|
@@ -27,6 +28,8 @@ export function processTranslationCall(tPath, config, output) {
|
|
|
27
28
|
options,
|
|
28
29
|
output,
|
|
29
30
|
config,
|
|
31
|
+
nodeLoc: tPath.parent.loc,
|
|
32
|
+
surroundingLineCount: SURROUNDING_LINE_COUNT,
|
|
30
33
|
});
|
|
31
34
|
// Route tx call to appropriate handler
|
|
32
35
|
routeTranslationCall({
|
|
@@ -23,6 +23,10 @@ export type ParsingConfig = {
|
|
|
23
23
|
* eg msg(['hello', 'world', 'foo', 'bar']) will not be registered
|
|
24
24
|
*/
|
|
25
25
|
ignoreInlineListContent: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* If true, include surrounding source code lines as context for translations
|
|
28
|
+
*/
|
|
29
|
+
includeSourceCodeContext?: boolean;
|
|
26
30
|
};
|
|
27
31
|
/**
|
|
28
32
|
* Mutable state for tracking parsing progress.
|
|
@@ -2,7 +2,7 @@ import { Updates } from '../../types/index.js';
|
|
|
2
2
|
import type { ParsingConfigOptions } from '../../types/parsing.js';
|
|
3
3
|
import { GTLibrary } from '../../types/libraries.js';
|
|
4
4
|
import { dedupeUpdates } from '../../extraction/postProcess.js';
|
|
5
|
-
export declare function createInlineUpdates(pkg: GTLibrary, validate: boolean, filePatterns: string[] | undefined, parsingOptions: ParsingConfigOptions): Promise<{
|
|
5
|
+
export declare function createInlineUpdates(pkg: GTLibrary, validate: boolean, filePatterns: string[] | undefined, parsingOptions: ParsingConfigOptions, includeSourceCodeContext?: boolean): Promise<{
|
|
6
6
|
updates: Updates;
|
|
7
7
|
errors: string[];
|
|
8
8
|
warnings: string[];
|
|
@@ -8,7 +8,7 @@ import { DEFAULT_SRC_PATTERNS } from '../../config/generateSettings.js';
|
|
|
8
8
|
import { getPathsAndAliases } from '../jsx/utils/getPathsAndAliases.js';
|
|
9
9
|
import { GT_LIBRARIES_UPSTREAM, REACT_LIBRARIES, } from '../../types/libraries.js';
|
|
10
10
|
import { calculateHashes, dedupeUpdates, linkStaticUpdates, } from '../../extraction/postProcess.js';
|
|
11
|
-
export async function createInlineUpdates(pkg, validate, filePatterns, parsingOptions) {
|
|
11
|
+
export async function createInlineUpdates(pkg, validate, filePatterns, parsingOptions, includeSourceCodeContext = false) {
|
|
12
12
|
const updates = [];
|
|
13
13
|
const errors = [];
|
|
14
14
|
const warnings = new Set();
|
|
@@ -39,6 +39,7 @@ export async function createInlineUpdates(pkg, validate, filePatterns, parsingOp
|
|
|
39
39
|
ignoreDynamicContent: false,
|
|
40
40
|
ignoreInvalidIcu: false,
|
|
41
41
|
ignoreInlineListContent: false,
|
|
42
|
+
includeSourceCodeContext,
|
|
42
43
|
}, { updates, errors, warnings });
|
|
43
44
|
}
|
|
44
45
|
// Parse <T> components
|
|
@@ -54,6 +55,7 @@ export async function createInlineUpdates(pkg, validate, filePatterns, parsingOp
|
|
|
54
55
|
parsingOptions,
|
|
55
56
|
pkgs,
|
|
56
57
|
file,
|
|
58
|
+
includeSourceCodeContext,
|
|
57
59
|
},
|
|
58
60
|
output: {
|
|
59
61
|
errors,
|
|
@@ -10,7 +10,7 @@ import { InlineLibrary } from '../types/libraries.js';
|
|
|
10
10
|
* @param pkg - The package name
|
|
11
11
|
* @returns An object containing the updates and errors
|
|
12
12
|
*/
|
|
13
|
-
export declare function createUpdates(options: TranslateFlags, src: string[] | undefined, sourceDictionary: string | undefined, pkg: InlineLibrary, validate: boolean, parsingOptions: ParsingConfigOptions): Promise<{
|
|
13
|
+
export declare function createUpdates(options: TranslateFlags, src: string[] | undefined, sourceDictionary: string | undefined, pkg: InlineLibrary, validate: boolean, parsingOptions: ParsingConfigOptions, includeSourceCodeContext?: boolean): Promise<{
|
|
14
14
|
updates: Updates;
|
|
15
15
|
errors: string[];
|
|
16
16
|
warnings: string[];
|
|
@@ -17,7 +17,7 @@ import { isPythonLibrary } from '../types/libraries.js';
|
|
|
17
17
|
* @param pkg - The package name
|
|
18
18
|
* @returns An object containing the updates and errors
|
|
19
19
|
*/
|
|
20
|
-
export async function createUpdates(options, src, sourceDictionary, pkg, validate, parsingOptions) {
|
|
20
|
+
export async function createUpdates(options, src, sourceDictionary, pkg, validate, parsingOptions, includeSourceCodeContext = false) {
|
|
21
21
|
let updates = [];
|
|
22
22
|
let errors = [];
|
|
23
23
|
let warnings = [];
|
|
@@ -53,7 +53,7 @@ export async function createUpdates(options, src, sourceDictionary, pkg, validat
|
|
|
53
53
|
// Scan through project for translatable content
|
|
54
54
|
const { updates: newUpdates, errors: newErrors, warnings: newWarnings, } = isPythonLibrary(pkg)
|
|
55
55
|
? await createPythonInlineUpdates(src)
|
|
56
|
-
: await createInlineUpdates(pkg, validate, src, parsingOptions);
|
|
56
|
+
: await createInlineUpdates(pkg, validate, src, parsingOptions, includeSourceCodeContext);
|
|
57
57
|
errors = [...errors, ...newErrors];
|
|
58
58
|
warnings = [...warnings, ...newWarnings];
|
|
59
59
|
updates = [...updates, ...newUpdates];
|
|
@@ -15,7 +15,7 @@ export async function aggregateInlineTranslations(options, settings, library) {
|
|
|
15
15
|
]);
|
|
16
16
|
}
|
|
17
17
|
// ---- CREATING UPDATES ---- //
|
|
18
|
-
const { updates, errors, warnings } = await createUpdates(options, settings.src, options.dictionary, library, false, settings.parsingOptions);
|
|
18
|
+
const { updates, errors, warnings } = await createUpdates(options, settings.src, options.dictionary, library, false, settings.parsingOptions, settings.options?.includeSourceCodeContext ?? false);
|
|
19
19
|
if (warnings.length > 0) {
|
|
20
20
|
logger.warn(chalk.yellow(`CLI tool encountered ${warnings.length} warnings while scanning for translatable content.\n` +
|
|
21
21
|
warnings
|
|
@@ -10,7 +10,7 @@ import { Libraries } from '../types/libraries.js';
|
|
|
10
10
|
*/
|
|
11
11
|
async function runValidation(settings, pkg, files) {
|
|
12
12
|
if (files && files.length > 0) {
|
|
13
|
-
return createInlineUpdates(pkg, true, files, settings.parsingOptions);
|
|
13
|
+
return createInlineUpdates(pkg, true, files, settings.parsingOptions, settings.options?.includeSourceCodeContext ?? false);
|
|
14
14
|
}
|
|
15
15
|
// Full project validation
|
|
16
16
|
// Use local variable to avoid mutating caller's settings object
|
|
@@ -23,7 +23,7 @@ async function runValidation(settings, pkg, files) {
|
|
|
23
23
|
'./dictionary.ts',
|
|
24
24
|
'./src/dictionary.ts',
|
|
25
25
|
]);
|
|
26
|
-
return createUpdates(settings, settings.src, dictionary, pkg, true, settings.parsingOptions);
|
|
26
|
+
return createUpdates(settings, settings.src, dictionary, pkg, true, settings.parsingOptions, settings.options?.includeSourceCodeContext ?? false);
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Parse file path from error/warning string in withLocation format: "filepath (line:col): message"
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export declare const TEMPLATE_FILE_NAME = "__INTERNAL_GT_TEMPLATE_NAME__";
|
|
|
4
4
|
export declare const TEMPLATE_FILE_ID: string;
|
|
5
5
|
export declare const DEFAULT_GIT_REMOTE_NAME = "origin";
|
|
6
6
|
export declare const DEFAULT_TIMEOUT_SECONDS = 900;
|
|
7
|
+
export declare const SURROUNDING_LINE_COUNT = 5;
|
package/dist/utils/constants.js
CHANGED
|
@@ -5,3 +5,5 @@ export const TEMPLATE_FILE_NAME = '__INTERNAL_GT_TEMPLATE_NAME__';
|
|
|
5
5
|
export const TEMPLATE_FILE_ID = hashStringSync(TEMPLATE_FILE_NAME);
|
|
6
6
|
export const DEFAULT_GIT_REMOTE_NAME = 'origin';
|
|
7
7
|
export const DEFAULT_TIMEOUT_SECONDS = 900;
|
|
8
|
+
// Number of source code lines to capture above and below a translation site
|
|
9
|
+
export const SURROUNDING_LINE_COUNT = 5;
|