gt 2.7.1 → 2.8.0-alpha.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.
Files changed (40) hide show
  1. package/dist/cli/base.js +1 -1
  2. package/dist/cli/inline.js +2 -0
  3. package/dist/cli/python.d.ts +10 -0
  4. package/dist/cli/python.js +9 -0
  5. package/dist/config/generateSettings.d.ts +2 -0
  6. package/dist/config/generateSettings.js +12 -2
  7. package/dist/extraction/index.d.ts +3 -0
  8. package/dist/extraction/index.js +2 -0
  9. package/dist/extraction/mapToUpdates.d.ts +6 -0
  10. package/dist/extraction/mapToUpdates.js +20 -0
  11. package/dist/extraction/postProcess.d.ts +14 -0
  12. package/dist/extraction/postProcess.js +78 -0
  13. package/dist/extraction/types.d.ts +1 -0
  14. package/dist/extraction/types.js +1 -0
  15. package/dist/formats/files/aggregateFiles.js +1 -1
  16. package/dist/fs/determineFramework/detectPythonLibrary.d.ts +6 -0
  17. package/dist/fs/determineFramework/detectPythonLibrary.js +36 -0
  18. package/dist/fs/{determineFramework.d.ts → determineFramework/index.d.ts} +1 -1
  19. package/dist/fs/determineFramework/index.js +62 -0
  20. package/dist/fs/determineFramework/matchPyprojectDependency.d.ts +9 -0
  21. package/dist/fs/determineFramework/matchPyprojectDependency.js +86 -0
  22. package/dist/fs/determineFramework/matchRequirementsTxtDependency.d.ts +6 -0
  23. package/dist/fs/determineFramework/matchRequirementsTxtDependency.js +21 -0
  24. package/dist/fs/determineFramework/matchSetupPyDependency.d.ts +7 -0
  25. package/dist/fs/determineFramework/matchSetupPyDependency.js +78 -0
  26. package/dist/fs/determineFramework/resolveGtDependency.d.ts +7 -0
  27. package/dist/fs/determineFramework/resolveGtDependency.js +17 -0
  28. package/dist/generated/version.d.ts +1 -1
  29. package/dist/generated/version.js +1 -1
  30. package/dist/index.js +6 -2
  31. package/dist/python/parse/createPythonInlineUpdates.d.ts +6 -0
  32. package/dist/python/parse/createPythonInlineUpdates.js +34 -0
  33. package/dist/react/parse/createInlineUpdates.d.ts +1 -4
  34. package/dist/react/parse/createInlineUpdates.js +1 -81
  35. package/dist/translation/parse.js +6 -2
  36. package/dist/types/index.d.ts +1 -1
  37. package/dist/types/libraries.d.ts +11 -3
  38. package/dist/types/libraries.js +18 -0
  39. package/package.json +3 -1
  40. package/dist/fs/determineFramework.js +0 -53
package/dist/cli/base.js CHANGED
@@ -31,7 +31,7 @@ import { setupLocadex } from '../locadex/setupFlow.js';
31
31
  import { detectFramework } from '../setup/detectFramework.js';
32
32
  import { getFrameworkDisplayName, getReactFrameworkLibrary, } from '../setup/frameworkUtils.js';
33
33
  import { findAgentFiles, findAgentFilesWithInstructions, hasCursorRulesDir, CURSOR_GT_RULES_FILE, getAgentInstructions, appendAgentInstructions, } from '../setup/agentInstructions.js';
34
- import { determineLibrary } from '../fs/determineFramework.js';
34
+ import { determineLibrary } from '../fs/determineFramework/index.js';
35
35
  import { INLINE_LIBRARIES } from '../types/libraries.js';
36
36
  import { handleEnqueue } from './commands/enqueue.js';
37
37
  export class BaseCLI {
@@ -129,6 +129,8 @@ function fallbackToGtReact(library) {
129
129
  Libraries.GT_NEXT,
130
130
  Libraries.GT_NODE,
131
131
  Libraries.GT_REACT_NATIVE,
132
+ Libraries.GT_FLASK,
133
+ Libraries.GT_FASTAPI,
132
134
  ].includes(library)
133
135
  ? library
134
136
  : Libraries.GT_REACT;
@@ -0,0 +1,10 @@
1
+ import { Command } from 'commander';
2
+ import { SupportedLibraries } from '../types/index.js';
3
+ import { InlineCLI } from './inline.js';
4
+ import { PythonLibrary } from '../types/libraries.js';
5
+ /**
6
+ * CLI tool for managing translations with gt-flask and gt-fastapi
7
+ */
8
+ export declare class PythonCLI extends InlineCLI {
9
+ constructor(command: Command, library: PythonLibrary, additionalModules?: SupportedLibraries[]);
10
+ }
@@ -0,0 +1,9 @@
1
+ import { InlineCLI } from './inline.js';
2
+ /**
3
+ * CLI tool for managing translations with gt-flask and gt-fastapi
4
+ */
5
+ export class PythonCLI extends InlineCLI {
6
+ constructor(command, library, additionalModules) {
7
+ super(command, library, additionalModules);
8
+ }
9
+ }
@@ -1,5 +1,7 @@
1
1
  import { Settings } from '../types/index.js';
2
2
  export declare const DEFAULT_SRC_PATTERNS: string[];
3
+ export declare const DEFAULT_PYTHON_SRC_PATTERNS: string[];
4
+ export declare const DEFAULT_PYTHON_SRC_EXCLUDES: string[];
3
5
  /**
4
6
  * Generates settings from any
5
7
  * @param flags - The CLI flags to generate settings from
@@ -18,6 +18,16 @@ export const DEFAULT_SRC_PATTERNS = [
18
18
  'pages/**/*.{js,jsx,ts,tsx}',
19
19
  'components/**/*.{js,jsx,ts,tsx}',
20
20
  ];
21
+ export const DEFAULT_PYTHON_SRC_PATTERNS = ['**/*.py'];
22
+ export const DEFAULT_PYTHON_SRC_EXCLUDES = [
23
+ 'venv/**',
24
+ '.venv/**',
25
+ '__pycache__/**',
26
+ '**/migrations/**',
27
+ '**/tests/**',
28
+ '**/test_*.py',
29
+ '**/*_test.py',
30
+ ];
21
31
  /**
22
32
  * Generates settings from any
23
33
  * @param flags - The CLI flags to generate settings from
@@ -105,8 +115,8 @@ export async function generateSettings(flags, cwd = process.cwd()) {
105
115
  mergedOptions.stageTranslations = mergedOptions.stageTranslations ?? false;
106
116
  // Add publish if not provided
107
117
  mergedOptions.publish = (gtConfig.publish || flags.publish) ?? false;
108
- // Populate src if not provided
109
- mergedOptions.src = mergedOptions.src || DEFAULT_SRC_PATTERNS;
118
+ // Don't default src here each pipeline (JS/Python) has its own defaults.
119
+ // Only set src if the user explicitly provided it via flags or config.
110
120
  // Resolve all glob patterns in the files object
111
121
  const compositePatterns = [
112
122
  ...Object.entries(mergedOptions.options?.jsonSchema || {}),
@@ -0,0 +1,3 @@
1
+ export type { ExtractionResult, ExtractionMetadata } from './types.js';
2
+ export { mapExtractionResultsToUpdates } from './mapToUpdates.js';
3
+ export { calculateHashes, dedupeUpdates, linkStaticUpdates, } from './postProcess.js';
@@ -0,0 +1,2 @@
1
+ export { mapExtractionResultsToUpdates } from './mapToUpdates.js';
2
+ export { calculateHashes, dedupeUpdates, linkStaticUpdates, } from './postProcess.js';
@@ -0,0 +1,6 @@
1
+ import type { ExtractionResult } from '@generaltranslation/python-extractor';
2
+ import type { Updates } from '../types/index.js';
3
+ /**
4
+ * Maps ExtractionResult[] to Updates[] format used by the CLI pipeline
5
+ */
6
+ export declare function mapExtractionResultsToUpdates(results: ExtractionResult[]): Updates;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Maps ExtractionResult[] to Updates[] format used by the CLI pipeline
3
+ */
4
+ export function mapExtractionResultsToUpdates(results) {
5
+ return results.map((result) => ({
6
+ dataFormat: result.dataFormat,
7
+ source: result.source,
8
+ metadata: {
9
+ ...(result.metadata.id && { id: result.metadata.id }),
10
+ ...(result.metadata.context && { context: result.metadata.context }),
11
+ ...(result.metadata.maxChars != null && {
12
+ maxChars: result.metadata.maxChars,
13
+ }),
14
+ ...(result.metadata.filePaths && {
15
+ filePaths: result.metadata.filePaths,
16
+ }),
17
+ ...(result.metadata.staticId && { staticId: result.metadata.staticId }),
18
+ },
19
+ }));
20
+ }
@@ -0,0 +1,14 @@
1
+ import { Updates } from '../types/index.js';
2
+ /**
3
+ * Calculate hashes for all updates in parallel
4
+ */
5
+ export declare function calculateHashes(updates: Updates): Promise<void>;
6
+ /**
7
+ * Dedupe entries by hash, merging filePaths
8
+ */
9
+ export declare function dedupeUpdates(updates: Updates): void;
10
+ /**
11
+ * Mark static updates as related by attaching a shared id to static content.
12
+ * Id is calculated as the hash of the static children's combined hashes.
13
+ */
14
+ export declare function linkStaticUpdates(updates: Updates): void;
@@ -0,0 +1,78 @@
1
+ import { hashSource, hashString } from 'generaltranslation/id';
2
+ /**
3
+ * Calculate hashes for all updates in parallel
4
+ */
5
+ export async function calculateHashes(updates) {
6
+ await Promise.all(updates.map(async (update) => {
7
+ const hash = hashSource({
8
+ source: update.source,
9
+ ...(update.metadata.context && { context: update.metadata.context }),
10
+ ...(update.metadata.id && { id: update.metadata.id }),
11
+ ...(update.metadata.maxChars != null && {
12
+ maxChars: update.metadata.maxChars,
13
+ }),
14
+ dataFormat: update.dataFormat,
15
+ });
16
+ update.metadata.hash = hash;
17
+ }));
18
+ }
19
+ /**
20
+ * Dedupe entries by hash, merging filePaths
21
+ */
22
+ export function dedupeUpdates(updates) {
23
+ const mergedByHash = new Map();
24
+ const noHashUpdates = [];
25
+ for (const update of updates) {
26
+ const hash = update.metadata.hash;
27
+ if (!hash) {
28
+ noHashUpdates.push(update);
29
+ continue;
30
+ }
31
+ const existing = mergedByHash.get(hash);
32
+ if (!existing) {
33
+ mergedByHash.set(hash, update);
34
+ continue;
35
+ }
36
+ const existingPaths = Array.isArray(existing.metadata.filePaths)
37
+ ? existing.metadata.filePaths.slice()
38
+ : [];
39
+ const newPaths = Array.isArray(update.metadata.filePaths)
40
+ ? update.metadata.filePaths
41
+ : [];
42
+ for (const p of newPaths) {
43
+ if (!existingPaths.includes(p)) {
44
+ existingPaths.push(p);
45
+ }
46
+ }
47
+ if (existingPaths.length) {
48
+ existing.metadata.filePaths = existingPaths;
49
+ }
50
+ }
51
+ const mergedUpdates = [...mergedByHash.values(), ...noHashUpdates];
52
+ updates.splice(0, updates.length, ...mergedUpdates);
53
+ }
54
+ /**
55
+ * Mark static updates as related by attaching a shared id to static content.
56
+ * Id is calculated as the hash of the static children's combined hashes.
57
+ */
58
+ export function linkStaticUpdates(updates) {
59
+ const temporaryStaticIdToUpdates = updates.reduce((acc, update) => {
60
+ if (update.metadata.staticId) {
61
+ if (!acc[update.metadata.staticId]) {
62
+ acc[update.metadata.staticId] = [];
63
+ }
64
+ acc[update.metadata.staticId].push(update);
65
+ }
66
+ return acc;
67
+ }, {});
68
+ Object.values(temporaryStaticIdToUpdates).forEach((staticUpdates) => {
69
+ const hashes = staticUpdates
70
+ .map((update) => update.metadata.hash)
71
+ .sort()
72
+ .join('-');
73
+ const sharedStaticId = hashString(hashes);
74
+ staticUpdates.forEach((update) => {
75
+ update.metadata.staticId = sharedStaticId;
76
+ });
77
+ });
78
+ }
@@ -0,0 +1 @@
1
+ export type { ExtractionResult, ExtractionMetadata, } from '@generaltranslation/python-extractor';
@@ -0,0 +1 @@
1
+ export {};
@@ -5,7 +5,7 @@ import { SUPPORTED_FILE_EXTENSIONS } from './supportedFiles.js';
5
5
  import { parseJson } from '../json/parseJson.js';
6
6
  import parseYaml from '../yaml/parseYaml.js';
7
7
  import YAML from 'yaml';
8
- import { determineLibrary } from '../../fs/determineFramework.js';
8
+ import { determineLibrary } from '../../fs/determineFramework/index.js';
9
9
  import { hashStringSync } from '../../utils/hash.js';
10
10
  import { preprocessContent } from './preprocessContent.js';
11
11
  export const SUPPORTED_DATA_FORMATS = ['JSX', 'ICU', 'I18NEXT'];
@@ -0,0 +1,6 @@
1
+ import { Libraries } from '../../types/libraries.js';
2
+ /**
3
+ * Detect Python GT library from pyproject.toml, requirements.txt, or setup.py.
4
+ * Checks files in priority order: pyproject.toml → requirements.txt → setup.py.
5
+ */
6
+ export declare function detectPythonLibrary(cwd: string): typeof Libraries.GT_FLASK | typeof Libraries.GT_FASTAPI | null;
@@ -0,0 +1,36 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { matchPyprojectDependency } from './matchPyprojectDependency.js';
4
+ import { matchRequirementsTxtDependency } from './matchRequirementsTxtDependency.js';
5
+ import { matchSetupPyDependency } from './matchSetupPyDependency.js';
6
+ /**
7
+ * Detect Python GT library from pyproject.toml, requirements.txt, or setup.py.
8
+ * Checks files in priority order: pyproject.toml → requirements.txt → setup.py.
9
+ */
10
+ export function detectPythonLibrary(cwd) {
11
+ // Check pyproject.toml
12
+ const pyprojectPath = path.join(cwd, 'pyproject.toml');
13
+ if (fs.existsSync(pyprojectPath)) {
14
+ const content = fs.readFileSync(pyprojectPath, 'utf8');
15
+ const result = matchPyprojectDependency(content);
16
+ if (result)
17
+ return result;
18
+ }
19
+ // Check requirements.txt
20
+ const requirementsPath = path.join(cwd, 'requirements.txt');
21
+ if (fs.existsSync(requirementsPath)) {
22
+ const content = fs.readFileSync(requirementsPath, 'utf8');
23
+ const result = matchRequirementsTxtDependency(content);
24
+ if (result)
25
+ return result;
26
+ }
27
+ // Check setup.py
28
+ const setupPath = path.join(cwd, 'setup.py');
29
+ if (fs.existsSync(setupPath)) {
30
+ const content = fs.readFileSync(setupPath, 'utf8');
31
+ const result = matchSetupPyDependency(content);
32
+ if (result)
33
+ return result;
34
+ }
35
+ return null;
36
+ }
@@ -1,4 +1,4 @@
1
- import { SupportedLibraries } from '../types/index.js';
1
+ import { SupportedLibraries } from '../../types/index.js';
2
2
  export declare function determineLibrary(): {
3
3
  library: SupportedLibraries;
4
4
  additionalModules: SupportedLibraries[];
@@ -0,0 +1,62 @@
1
+ import chalk from 'chalk';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import { logger } from '../../console/logger.js';
5
+ import { Libraries } from '../../types/libraries.js';
6
+ import { detectPythonLibrary } from './detectPythonLibrary.js';
7
+ export function determineLibrary() {
8
+ let library = 'base';
9
+ const additionalModules = [];
10
+ try {
11
+ // Get the current working directory (where the CLI is being run)
12
+ const cwd = process.cwd();
13
+ const packageJsonPath = path.join(cwd, 'package.json');
14
+ // Check if package.json exists
15
+ if (fs.existsSync(packageJsonPath)) {
16
+ // Read and parse package.json
17
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
18
+ const dependencies = {
19
+ ...packageJson.dependencies,
20
+ ...packageJson.devDependencies,
21
+ };
22
+ // Check for gt-next or gt-react in dependencies
23
+ if (dependencies[Libraries.GT_NEXT]) {
24
+ library = Libraries.GT_NEXT;
25
+ }
26
+ else if (dependencies[Libraries.GT_REACT]) {
27
+ library = Libraries.GT_REACT;
28
+ }
29
+ else if (dependencies[Libraries.GT_REACT_NATIVE]) {
30
+ library = Libraries.GT_REACT_NATIVE;
31
+ }
32
+ else if (dependencies[Libraries.GT_NODE]) {
33
+ library = Libraries.GT_NODE;
34
+ }
35
+ else if (dependencies['next-intl']) {
36
+ library = 'next-intl';
37
+ }
38
+ else if (dependencies['i18next']) {
39
+ library = 'i18next';
40
+ }
41
+ if (dependencies['i18next-icu']) {
42
+ additionalModules.push('i18next-icu');
43
+ }
44
+ }
45
+ // If no JS library found, check for Python project files
46
+ if (library === 'base') {
47
+ const pythonLibrary = detectPythonLibrary(cwd);
48
+ if (pythonLibrary) {
49
+ library = pythonLibrary;
50
+ }
51
+ else if (!fs.existsSync(path.join(cwd, 'package.json'))) {
52
+ logger.warn(chalk.yellow('No package.json or Python project file found in the current directory. Run this command from the root of your project.'));
53
+ }
54
+ }
55
+ // Fallback to base if neither is found
56
+ return { library, additionalModules };
57
+ }
58
+ catch (error) {
59
+ logger.error('Error determining framework: ' + String(error));
60
+ return { library: 'base', additionalModules: [] };
61
+ }
62
+ }
@@ -0,0 +1,9 @@
1
+ import { Libraries } from '../../types/libraries.js';
2
+ /**
3
+ * Parse pyproject.toml for GT dependencies using a proper TOML parser.
4
+ *
5
+ * Checks the following locations:
6
+ * - PEP 621: project.dependencies, project.optional-dependencies.*
7
+ * - Poetry: tool.poetry.dependencies, tool.poetry.group.*.dependencies
8
+ */
9
+ export declare function matchPyprojectDependency(content: string): typeof Libraries.GT_FLASK | typeof Libraries.GT_FASTAPI | null;
@@ -0,0 +1,86 @@
1
+ import { parse } from 'smol-toml';
2
+ import { resolveGtDependency } from './resolveGtDependency.js';
3
+ /**
4
+ * Parse pyproject.toml for GT dependencies using a proper TOML parser.
5
+ *
6
+ * Checks the following locations:
7
+ * - PEP 621: project.dependencies, project.optional-dependencies.*
8
+ * - Poetry: tool.poetry.dependencies, tool.poetry.group.*.dependencies
9
+ */
10
+ export function matchPyprojectDependency(content) {
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ let parsed;
13
+ try {
14
+ parsed = parse(content);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ // 1. PEP 621: project.dependencies = ["gt-flask>=1.0", ...]
20
+ const projectDeps = parsed?.project?.dependencies;
21
+ if (Array.isArray(projectDeps)) {
22
+ const result = matchDependencyArray(projectDeps);
23
+ if (result)
24
+ return result;
25
+ }
26
+ // 2. PEP 621: project.optional-dependencies.* = ["gt-flask", ...]
27
+ const optDeps = parsed?.project?.['optional-dependencies'];
28
+ if (optDeps && typeof optDeps === 'object') {
29
+ for (const group of Object.values(optDeps)) {
30
+ if (Array.isArray(group)) {
31
+ const result = matchDependencyArray(group);
32
+ if (result)
33
+ return result;
34
+ }
35
+ }
36
+ }
37
+ // 3. Poetry: tool.poetry.dependencies = { gt-flask = "^1.0", ... }
38
+ const poetryDeps = parsed?.tool?.poetry?.dependencies;
39
+ if (poetryDeps && typeof poetryDeps === 'object') {
40
+ const result = matchDependencyKeys(poetryDeps);
41
+ if (result)
42
+ return result;
43
+ }
44
+ // 4. Poetry groups: tool.poetry.group.*.dependencies
45
+ const poetryGroups = parsed?.tool?.poetry?.group;
46
+ if (poetryGroups && typeof poetryGroups === 'object') {
47
+ for (const group of Object.values(poetryGroups)) {
48
+ if (group && typeof group === 'object' && 'dependencies' in group) {
49
+ const result = matchDependencyKeys(group.dependencies);
50
+ if (result)
51
+ return result;
52
+ }
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * Check a PEP 508 dependency array (e.g. ["gt-flask>=1.0", "flask[extra]"])
59
+ * for GT dependencies.
60
+ */
61
+ function matchDependencyArray(deps) {
62
+ for (const dep of deps) {
63
+ if (typeof dep !== 'string')
64
+ continue;
65
+ // Extract package name before version specifiers, extras, or markers
66
+ const pkgName = dep.split(/[><=!~;@\s[]/)[0].trim();
67
+ if (pkgName) {
68
+ const result = resolveGtDependency(pkgName);
69
+ if (result)
70
+ return result;
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+ /**
76
+ * Check Poetry-style dependency keys (e.g. { gt-flask = "^1.0" })
77
+ * for GT dependencies.
78
+ */
79
+ function matchDependencyKeys(deps) {
80
+ for (const key of Object.keys(deps)) {
81
+ const result = resolveGtDependency(key);
82
+ if (result)
83
+ return result;
84
+ }
85
+ return null;
86
+ }
@@ -0,0 +1,6 @@
1
+ import { Libraries } from '../../types/libraries.js';
2
+ /**
3
+ * Parse requirements.txt for GT dependencies.
4
+ * Each line is a package specification: package[extras]>=version
5
+ */
6
+ export declare function matchRequirementsTxtDependency(content: string): typeof Libraries.GT_FLASK | typeof Libraries.GT_FASTAPI | null;
@@ -0,0 +1,21 @@
1
+ import { resolveGtDependency } from './resolveGtDependency.js';
2
+ /**
3
+ * Parse requirements.txt for GT dependencies.
4
+ * Each line is a package specification: package[extras]>=version
5
+ */
6
+ export function matchRequirementsTxtDependency(content) {
7
+ const lines = content.split('\n');
8
+ for (const line of lines) {
9
+ const trimmed = line.split('#')[0].trim();
10
+ if (!trimmed || trimmed.startsWith('-'))
11
+ continue;
12
+ // Extract package name before version specifiers, extras, or markers
13
+ const pkgName = trimmed.split(/[><=!~;@\s[]/)[0].trim();
14
+ if (pkgName) {
15
+ const result = resolveGtDependency(pkgName);
16
+ if (result)
17
+ return result;
18
+ }
19
+ }
20
+ return null;
21
+ }
@@ -0,0 +1,7 @@
1
+ import { Libraries } from '../../types/libraries.js';
2
+ /**
3
+ * Parse setup.py for GT dependencies.
4
+ * Extracts quoted strings from install_requires or extras_require blocks
5
+ * and matches against GT packages.
6
+ */
7
+ export declare function matchSetupPyDependency(content: string): typeof Libraries.GT_FLASK | typeof Libraries.GT_FASTAPI | null;
@@ -0,0 +1,78 @@
1
+ import { resolveGtDependency } from './resolveGtDependency.js';
2
+ /**
3
+ * Parse setup.py for GT dependencies.
4
+ * Extracts quoted strings from install_requires or extras_require blocks
5
+ * and matches against GT packages.
6
+ */
7
+ export function matchSetupPyDependency(content) {
8
+ // Find install_requires=[...] and extras_require={...:[...]} blocks
9
+ // and extract quoted dependency strings from within them.
10
+ // We search for the keyword, then collect quoted strings until the
11
+ // corresponding closing bracket.
12
+ const dependencyKeywords = ['install_requires', 'extras_require'];
13
+ for (const keyword of dependencyKeywords) {
14
+ const keywordIndex = content.indexOf(keyword);
15
+ if (keywordIndex === -1)
16
+ continue;
17
+ // Find the opening bracket after the keyword
18
+ const afterKeyword = content.slice(keywordIndex + keyword.length);
19
+ const bracketMatch = afterKeyword.match(/\s*=\s*[\[{]/);
20
+ if (!bracketMatch)
21
+ continue;
22
+ const openBracket = afterKeyword[bracketMatch.index + bracketMatch[0].length - 1];
23
+ const closeBracket = openBracket === '[' ? ']' : '}';
24
+ // Extract the content between brackets
25
+ const startIndex = keywordIndex +
26
+ keyword.length +
27
+ bracketMatch.index +
28
+ bracketMatch[0].length;
29
+ const closeIndex = findMatchingBracket(content, startIndex - 1, openBracket, closeBracket);
30
+ if (closeIndex === -1)
31
+ continue;
32
+ const blockContent = content.slice(startIndex, closeIndex);
33
+ // Extract all quoted strings from the block
34
+ const quotedStrings = blockContent.match(/["']([^"']+)["']/g);
35
+ if (!quotedStrings)
36
+ continue;
37
+ for (const match of quotedStrings) {
38
+ const value = match.slice(1, -1);
39
+ const pkgName = value.split(/[><=!~;@\s[]/)[0].trim();
40
+ if (pkgName) {
41
+ const result = resolveGtDependency(pkgName);
42
+ if (result)
43
+ return result;
44
+ }
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ /**
50
+ * Find the matching closing bracket, handling nesting.
51
+ */
52
+ function findMatchingBracket(content, startIndex, openBracket, closeBracket) {
53
+ let depth = 0;
54
+ let inString = false;
55
+ let stringChar = '';
56
+ for (let i = startIndex; i < content.length; i++) {
57
+ const ch = content[i];
58
+ // Track string boundaries to avoid counting brackets inside strings
59
+ if (!inString && (ch === '"' || ch === "'")) {
60
+ inString = true;
61
+ stringChar = ch;
62
+ }
63
+ else if (inString && ch === stringChar && content[i - 1] !== '\\') {
64
+ inString = false;
65
+ }
66
+ if (inString)
67
+ continue;
68
+ if (ch === openBracket) {
69
+ depth++;
70
+ }
71
+ else if (ch === closeBracket) {
72
+ depth--;
73
+ if (depth === 0)
74
+ return i;
75
+ }
76
+ }
77
+ return -1;
78
+ }
@@ -0,0 +1,7 @@
1
+ import { Libraries } from '../../types/libraries.js';
2
+ /**
3
+ * Resolve a dependency name (hyphenated or underscored) to a Python GT library.
4
+ * Per PEP 503, Python package names are normalized: hyphens, underscores, and
5
+ * periods are interchangeable. We match both gt-flask/gt_flask forms.
6
+ */
7
+ export declare function resolveGtDependency(pkgName: string): typeof Libraries.GT_FLASK | typeof Libraries.GT_FASTAPI | null;
@@ -0,0 +1,17 @@
1
+ import { Libraries } from '../../types/libraries.js';
2
+ import { PYTHON_GT_DEPENDENCIES } from '@generaltranslation/python-extractor';
3
+ /**
4
+ * Resolve a dependency name (hyphenated or underscored) to a Python GT library.
5
+ * Per PEP 503, Python package names are normalized: hyphens, underscores, and
6
+ * periods are interchangeable. We match both gt-flask/gt_flask forms.
7
+ */
8
+ export function resolveGtDependency(pkgName) {
9
+ // Normalize: replace underscores/periods with hyphens, lowercase
10
+ const normalized = pkgName.toLowerCase().replace(/[_.]/g, '-');
11
+ for (const dep of PYTHON_GT_DEPENDENCIES) {
12
+ if (normalized === dep) {
13
+ return dep === 'gt-flask' ? Libraries.GT_FLASK : Libraries.GT_FASTAPI;
14
+ }
15
+ }
16
+ return null;
17
+ }
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "2.7.1";
1
+ export declare const PACKAGE_VERSION = "2.8.0-alpha.0";
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
- export const PACKAGE_VERSION = '2.7.1';
2
+ export const PACKAGE_VERSION = '2.8.0-alpha.0';
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { BaseCLI } from './cli/base.js';
2
2
  import { NextCLI } from './cli/next.js';
3
3
  import { ReactCLI } from './cli/react.js';
4
- import { determineLibrary } from './fs/determineFramework.js';
4
+ import { PythonCLI } from './cli/python.js';
5
+ import { determineLibrary } from './fs/determineFramework/index.js';
5
6
  import { NodeCLI } from './cli/node.js';
6
- import { Libraries } from './types/libraries.js';
7
+ import { Libraries, isPythonLibrary } from './types/libraries.js';
7
8
  export function main(program) {
8
9
  program.name('gt');
9
10
  const { library, additionalModules } = determineLibrary();
@@ -18,6 +19,9 @@ export function main(program) {
18
19
  else if (library === Libraries.GT_NODE) {
19
20
  cli = new NodeCLI(program, library, additionalModules);
20
21
  }
22
+ else if (isPythonLibrary(library)) {
23
+ cli = new PythonCLI(program, library, additionalModules);
24
+ }
21
25
  else {
22
26
  cli = new BaseCLI(program, library, additionalModules);
23
27
  }
@@ -0,0 +1,6 @@
1
+ import { Updates } from '../../types/index.js';
2
+ export declare function createPythonInlineUpdates(filePatterns: string[] | undefined): Promise<{
3
+ updates: Updates;
4
+ errors: string[];
5
+ warnings: string[];
6
+ }>;
@@ -0,0 +1,34 @@
1
+ import fs from 'node:fs';
2
+ import { extractFromPythonSource } from '@generaltranslation/python-extractor';
3
+ import { mapExtractionResultsToUpdates } from '../../extraction/mapToUpdates.js';
4
+ import { calculateHashes, dedupeUpdates, linkStaticUpdates, } from '../../extraction/postProcess.js';
5
+ import { matchFiles } from '../../fs/matchFiles.js';
6
+ import { DEFAULT_PYTHON_SRC_PATTERNS, DEFAULT_PYTHON_SRC_EXCLUDES, } from '../../config/generateSettings.js';
7
+ export async function createPythonInlineUpdates(filePatterns) {
8
+ const updates = [];
9
+ const errors = [];
10
+ const warnings = [];
11
+ // Match Python source files, excluding common non-source directories
12
+ const patterns = filePatterns || DEFAULT_PYTHON_SRC_PATTERNS;
13
+ const files = matchFiles(process.cwd(), [
14
+ ...patterns,
15
+ ...DEFAULT_PYTHON_SRC_EXCLUDES.map((p) => `!${p}`),
16
+ ]);
17
+ for (const file of files) {
18
+ const code = await fs.promises.readFile(file, 'utf8');
19
+ try {
20
+ const { results, errors: fileErrors, warnings: fileWarnings, } = await extractFromPythonSource(code, file);
21
+ updates.push(...mapExtractionResultsToUpdates(results));
22
+ errors.push(...fileErrors);
23
+ warnings.push(...fileWarnings);
24
+ }
25
+ catch (error) {
26
+ errors.push(`Error extracting from ${file}: ${String(error)}`);
27
+ }
28
+ }
29
+ // Post processing steps
30
+ await calculateHashes(updates);
31
+ dedupeUpdates(updates);
32
+ linkStaticUpdates(updates);
33
+ return { updates, errors, warnings };
34
+ }
@@ -1,13 +1,10 @@
1
1
  import { Updates } from '../../types/index.js';
2
2
  import type { ParsingConfigOptions } from '../../types/parsing.js';
3
3
  import { GTLibrary } from '../../types/libraries.js';
4
+ import { dedupeUpdates } from '../../extraction/postProcess.js';
4
5
  export declare function createInlineUpdates(pkg: GTLibrary, validate: boolean, filePatterns: string[] | undefined, parsingOptions: ParsingConfigOptions): Promise<{
5
6
  updates: Updates;
6
7
  errors: string[];
7
8
  warnings: string[];
8
9
  }>;
9
- /**
10
- * Dedupe entries
11
- */
12
- declare function dedupeUpdates(updates: Updates): void;
13
10
  export { dedupeUpdates as _test_dedupeUpdates };
@@ -1,6 +1,5 @@
1
1
  import fs from 'node:fs';
2
2
  import { parse } from '@babel/parser';
3
- import { hashSource, hashString } from 'generaltranslation/id';
4
3
  import { parseTranslationComponent } from '../jsx/utils/jsxParsing/parseJsx.js';
5
4
  import { parseStrings } from '../jsx/utils/parseStringFunction.js';
6
5
  import { logger } from '../../console/logger.js';
@@ -8,6 +7,7 @@ import { matchFiles } from '../../fs/matchFiles.js';
8
7
  import { DEFAULT_SRC_PATTERNS } from '../../config/generateSettings.js';
9
8
  import { getPathsAndAliases } from '../jsx/utils/getPathsAndAliases.js';
10
9
  import { GT_LIBRARIES_UPSTREAM, REACT_LIBRARIES, } from '../../types/libraries.js';
10
+ import { calculateHashes, dedupeUpdates, linkStaticUpdates, } from '../../extraction/postProcess.js';
11
11
  export async function createInlineUpdates(pkg, validate, filePatterns, parsingOptions) {
12
12
  const updates = [];
13
13
  const errors = [];
@@ -78,84 +78,4 @@ export async function createInlineUpdates(pkg, validate, filePatterns, parsingOp
78
78
  function getUpstreamPackages(pkg) {
79
79
  return GT_LIBRARIES_UPSTREAM[pkg];
80
80
  }
81
- /**
82
- * Calculate hashes
83
- */
84
- async function calculateHashes(updates) {
85
- // parallel calculation of hashes
86
- await Promise.all(updates.map(async (update) => {
87
- const hash = hashSource({
88
- source: update.source,
89
- ...(update.metadata.context && { context: update.metadata.context }),
90
- ...(update.metadata.id && { id: update.metadata.id }),
91
- ...(update.metadata.maxChars != null && {
92
- maxChars: update.metadata.maxChars,
93
- }),
94
- dataFormat: update.dataFormat,
95
- });
96
- update.metadata.hash = hash;
97
- }));
98
- }
99
- /**
100
- * Dedupe entries
101
- */
102
- function dedupeUpdates(updates) {
103
- const mergedByHash = new Map();
104
- const noHashUpdates = [];
105
- for (const update of updates) {
106
- const hash = update.metadata.hash;
107
- if (!hash) {
108
- noHashUpdates.push(update);
109
- continue;
110
- }
111
- const existing = mergedByHash.get(hash);
112
- if (!existing) {
113
- mergedByHash.set(hash, update);
114
- continue;
115
- }
116
- const existingPaths = Array.isArray(existing.metadata.filePaths)
117
- ? existing.metadata.filePaths.slice()
118
- : [];
119
- const newPaths = Array.isArray(update.metadata.filePaths)
120
- ? update.metadata.filePaths
121
- : [];
122
- for (const p of newPaths) {
123
- if (!existingPaths.includes(p)) {
124
- existingPaths.push(p);
125
- }
126
- }
127
- if (existingPaths.length) {
128
- existing.metadata.filePaths = existingPaths;
129
- }
130
- }
131
- const mergedUpdates = [...mergedByHash.values(), ...noHashUpdates];
132
- updates.splice(0, updates.length, ...mergedUpdates);
133
- }
134
- /**
135
- * Mark static updates as the related by attaching a shared id to static content
136
- * Id is calculated as the hash of the static children's combined hashes
137
- */
138
- function linkStaticUpdates(updates) {
139
- // construct map of temporary static ids to updates
140
- const temporaryStaticIdToUpdates = updates.reduce((acc, update) => {
141
- if (update.metadata.staticId) {
142
- if (!acc[update.metadata.staticId]) {
143
- acc[update.metadata.staticId] = [];
144
- }
145
- acc[update.metadata.staticId].push(update);
146
- }
147
- return acc;
148
- }, {});
149
- // Calculate shared static ids
150
- Object.values(temporaryStaticIdToUpdates).forEach((staticUpdates) => {
151
- const hashes = staticUpdates
152
- .map((update) => update.metadata.hash)
153
- .sort()
154
- .join('-');
155
- const sharedStaticId = hashString(hashes);
156
- staticUpdates.forEach((update) => {
157
- update.metadata.staticId = sharedStaticId;
158
- });
159
- });
160
- }
161
81
  export { dedupeUpdates as _test_dedupeUpdates };
@@ -3,9 +3,11 @@ import { logger } from '../console/logger.js';
3
3
  import loadJSON from '../fs/loadJSON.js';
4
4
  import { createDictionaryUpdates } from '../react/parse/createDictionaryUpdates.js';
5
5
  import { createInlineUpdates } from '../react/parse/createInlineUpdates.js';
6
+ import { createPythonInlineUpdates } from '../python/parse/createPythonInlineUpdates.js';
6
7
  import createESBuildConfig from '../react/config/createESBuildConfig.js';
7
8
  import chalk from 'chalk';
8
9
  import { exitSync } from '../console/logging.js';
10
+ import { isPythonLibrary } from '../types/libraries.js';
9
11
  /**
10
12
  * Searches for gt-react or gt-next dictionary files and creates updates for them,
11
13
  * as well as inline updates for <T> tags and useGT()/getGT() calls
@@ -48,8 +50,10 @@ export async function createUpdates(options, src, sourceDictionary, pkg, validat
48
50
  ];
49
51
  }
50
52
  }
51
- // Scan through project for <T> tags
52
- const { updates: newUpdates, errors: newErrors, warnings: newWarnings, } = await createInlineUpdates(pkg, validate, src, parsingOptions);
53
+ // Scan through project for translatable content
54
+ const { updates: newUpdates, errors: newErrors, warnings: newWarnings, } = isPythonLibrary(pkg)
55
+ ? await createPythonInlineUpdates(src)
56
+ : await createInlineUpdates(pkg, validate, src, parsingOptions);
53
57
  errors = [...errors, ...newErrors];
54
58
  warnings = [...warnings, ...newWarnings];
55
59
  updates = [...updates, ...newUpdates];
@@ -164,7 +164,7 @@ export type Settings = {
164
164
  _versionId?: string;
165
165
  version?: string;
166
166
  description?: string;
167
- src: string[];
167
+ src?: string[];
168
168
  framework?: SupportedFrameworks;
169
169
  options?: AdditionalOptions;
170
170
  modelProvider?: string;
@@ -7,17 +7,19 @@ export declare enum Libraries {
7
7
  GT_REACT_NATIVE = "gt-react-native",
8
8
  GT_NODE = "gt-node",
9
9
  GT_I18N = "gt-i18n",
10
- GT_REACT_CORE = "@generaltranslation/react-core"
10
+ GT_REACT_CORE = "@generaltranslation/react-core",
11
+ GT_FLASK = "gt-flask",
12
+ GT_FASTAPI = "gt-fastapi"
11
13
  }
12
14
  /**
13
15
  * A list of all the libraries that support the CLI
14
16
  */
15
- export declare const GT_LIBRARIES: readonly [Libraries.GT_REACT, Libraries.GT_NEXT, Libraries.GT_REACT_NATIVE, Libraries.GT_NODE, Libraries.GT_I18N, Libraries.GT_REACT_CORE];
17
+ export declare const GT_LIBRARIES: readonly [Libraries.GT_REACT, Libraries.GT_NEXT, Libraries.GT_REACT_NATIVE, Libraries.GT_NODE, Libraries.GT_I18N, Libraries.GT_REACT_CORE, Libraries.GT_FLASK, Libraries.GT_FASTAPI];
16
18
  export type GTLibrary = (typeof GT_LIBRARIES)[number];
17
19
  /**
18
20
  * Libraries that support inline translation
19
21
  */
20
- export declare const INLINE_LIBRARIES: readonly [Libraries.GT_REACT, Libraries.GT_NEXT, Libraries.GT_NODE, Libraries.GT_REACT_NATIVE, Libraries.GT_REACT_CORE, Libraries.GT_I18N];
22
+ export declare const INLINE_LIBRARIES: readonly [Libraries.GT_REACT, Libraries.GT_NEXT, Libraries.GT_NODE, Libraries.GT_REACT_NATIVE, Libraries.GT_REACT_CORE, Libraries.GT_I18N, Libraries.GT_FLASK, Libraries.GT_FASTAPI];
21
23
  export type InlineLibrary = (typeof INLINE_LIBRARIES)[number];
22
24
  export declare function isInlineLibrary(lib: string): lib is InlineLibrary;
23
25
  /**
@@ -25,6 +27,12 @@ export declare function isInlineLibrary(lib: string): lib is InlineLibrary;
25
27
  */
26
28
  export declare const REACT_LIBRARIES: readonly [Libraries.GT_NEXT, Libraries.GT_REACT, Libraries.GT_REACT_NATIVE, Libraries.GT_REACT_CORE];
27
29
  export type ReactLibrary = (typeof REACT_LIBRARIES)[number];
30
+ /**
31
+ * Python libraries
32
+ */
33
+ export declare const PYTHON_LIBRARIES: readonly [Libraries.GT_FLASK, Libraries.GT_FASTAPI];
34
+ export type PythonLibrary = (typeof PYTHON_LIBRARIES)[number];
35
+ export declare function isPythonLibrary(lib: string): lib is PythonLibrary;
28
36
  /**
29
37
  * A mapping of each library to their upstream dependencies for filtering imports
30
38
  */
@@ -9,6 +9,8 @@ export var Libraries;
9
9
  Libraries["GT_NODE"] = "gt-node";
10
10
  Libraries["GT_I18N"] = "gt-i18n";
11
11
  Libraries["GT_REACT_CORE"] = "@generaltranslation/react-core";
12
+ Libraries["GT_FLASK"] = "gt-flask";
13
+ Libraries["GT_FASTAPI"] = "gt-fastapi";
12
14
  })(Libraries || (Libraries = {}));
13
15
  /**
14
16
  * A list of all the libraries that support the CLI
@@ -20,6 +22,8 @@ export const GT_LIBRARIES = [
20
22
  Libraries.GT_NODE,
21
23
  Libraries.GT_I18N,
22
24
  Libraries.GT_REACT_CORE,
25
+ Libraries.GT_FLASK,
26
+ Libraries.GT_FASTAPI,
23
27
  ];
24
28
  /**
25
29
  * Libraries that support inline translation
@@ -31,6 +35,8 @@ export const INLINE_LIBRARIES = [
31
35
  Libraries.GT_REACT_NATIVE,
32
36
  Libraries.GT_REACT_CORE,
33
37
  Libraries.GT_I18N,
38
+ Libraries.GT_FLASK,
39
+ Libraries.GT_FASTAPI,
34
40
  ];
35
41
  export function isInlineLibrary(lib) {
36
42
  return INLINE_LIBRARIES.includes(lib);
@@ -44,6 +50,16 @@ export const REACT_LIBRARIES = [
44
50
  Libraries.GT_REACT_NATIVE,
45
51
  Libraries.GT_REACT_CORE,
46
52
  ];
53
+ /**
54
+ * Python libraries
55
+ */
56
+ export const PYTHON_LIBRARIES = [
57
+ Libraries.GT_FLASK,
58
+ Libraries.GT_FASTAPI,
59
+ ];
60
+ export function isPythonLibrary(lib) {
61
+ return PYTHON_LIBRARIES.includes(lib);
62
+ }
47
63
  /**
48
64
  * A mapping of each library to their upstream dependencies for filtering imports
49
65
  */
@@ -69,4 +85,6 @@ export const GT_LIBRARIES_UPSTREAM = {
69
85
  [Libraries.GT_NODE]: [Libraries.GT_I18N, Libraries.GT_NODE],
70
86
  [Libraries.GT_REACT_CORE]: [Libraries.GT_I18N, Libraries.GT_REACT_CORE],
71
87
  [Libraries.GT_I18N]: [Libraries.GT_I18N],
88
+ [Libraries.GT_FLASK]: [Libraries.GT_FLASK],
89
+ [Libraries.GT_FASTAPI]: [Libraries.GT_FASTAPI],
72
90
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gt",
3
- "version": "2.7.1",
3
+ "version": "2.8.0-alpha.0",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [
@@ -92,6 +92,7 @@
92
92
  "esbuild": "^0.27.2",
93
93
  "fast-glob": "^3.3.3",
94
94
  "fast-json-stable-stringify": "^2.1.0",
95
+ "@generaltranslation/python-extractor": "0.0.1-alpha.0",
95
96
  "html-entities": "^2.6.0",
96
97
  "json-pointer": "^0.6.2",
97
98
  "jsonpath-plus": "^10.3.0",
@@ -105,6 +106,7 @@
105
106
  "remark-parse": "^11.0.0",
106
107
  "remark-stringify": "^11.0.0",
107
108
  "resolve": "^1.22.10",
109
+ "smol-toml": "^1.3.1",
108
110
  "tsconfig-paths": "^4.2.0",
109
111
  "unified": "^11.0.5",
110
112
  "unist-util-visit": "^5.0.0",
@@ -1,53 +0,0 @@
1
- import chalk from 'chalk';
2
- import path from 'node:path';
3
- import fs from 'node:fs';
4
- import { logger } from '../console/logger.js';
5
- import { Libraries } from '../types/libraries.js';
6
- export function determineLibrary() {
7
- let library = 'base';
8
- const additionalModules = [];
9
- try {
10
- // Get the current working directory (where the CLI is being run)
11
- const cwd = process.cwd();
12
- const packageJsonPath = path.join(cwd, 'package.json');
13
- // Check if package.json exists
14
- if (!fs.existsSync(packageJsonPath)) {
15
- logger.warn(chalk.yellow('No package.json found in the current directory. Run this command from the root of your project.'));
16
- return { library: 'base', additionalModules: [] };
17
- }
18
- // Read and parse package.json
19
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
20
- const dependencies = {
21
- ...packageJson.dependencies,
22
- ...packageJson.devDependencies,
23
- };
24
- // Check for gt-next or gt-react in dependencies
25
- if (dependencies[Libraries.GT_NEXT]) {
26
- library = Libraries.GT_NEXT;
27
- }
28
- else if (dependencies[Libraries.GT_REACT]) {
29
- library = Libraries.GT_REACT;
30
- }
31
- else if (dependencies[Libraries.GT_REACT_NATIVE]) {
32
- library = Libraries.GT_REACT_NATIVE;
33
- }
34
- else if (dependencies[Libraries.GT_NODE]) {
35
- library = Libraries.GT_NODE;
36
- }
37
- else if (dependencies['next-intl']) {
38
- library = 'next-intl';
39
- }
40
- else if (dependencies['i18next']) {
41
- library = 'i18next';
42
- }
43
- if (dependencies['i18next-icu']) {
44
- additionalModules.push('i18next-icu');
45
- }
46
- // Fallback to base if neither is found
47
- return { library, additionalModules };
48
- }
49
- catch (error) {
50
- logger.error('Error determining framework: ' + String(error));
51
- return { library: 'base', additionalModules: [] };
52
- }
53
- }