Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
"esbuild": "^0.27.2",
"fast-glob": "^3.3.3",
"fast-json-stable-stringify": "^2.1.0",
"@generaltranslation/python-extractor": "workspace:*",
"generaltranslation": "workspace:*",
"gt-remark": "workspace:*",
"html-entities": "^2.6.0",
Expand All @@ -132,6 +133,7 @@
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"resolve": "^1.22.10",
"smol-toml": "^1.3.1",
"tsconfig-paths": "^4.2.0",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/cli/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import {
getAgentInstructions,
appendAgentInstructions,
} from '../setup/agentInstructions.js';
import { determineLibrary } from '../fs/determineFramework.js';
import { determineLibrary } from '../fs/determineFramework/index.js';
import { INLINE_LIBRARIES } from '../types/libraries.js';
import { handleEnqueue } from './commands/enqueue.js';

Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/cli/inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,14 @@ function fallbackToGtReact(library: SupportedLibraries): InlineLibrary {
Libraries.GT_NEXT,
Libraries.GT_NODE,
Libraries.GT_REACT_NATIVE,
Libraries.GT_FLASK,
Libraries.GT_FASTAPI,
].includes(library as Libraries)
? (library as
| typeof Libraries.GT_NEXT
| typeof Libraries.GT_NODE
| typeof Libraries.GT_REACT_NATIVE)
| typeof Libraries.GT_REACT_NATIVE
| typeof Libraries.GT_FLASK
| typeof Libraries.GT_FASTAPI)
: Libraries.GT_REACT;
}
17 changes: 17 additions & 0 deletions packages/cli/src/cli/python.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Command } from 'commander';
import { SupportedLibraries } from '../types/index.js';
import { InlineCLI } from './inline.js';
import { PythonLibrary } from '../types/libraries.js';

/**
* CLI tool for managing translations with gt-flask and gt-fastapi
*/
export class PythonCLI extends InlineCLI {
constructor(
command: Command,
library: PythonLibrary,
additionalModules?: SupportedLibraries[]
) {
super(command, library, additionalModules);
}
}
15 changes: 13 additions & 2 deletions packages/cli/src/config/generateSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ export const DEFAULT_SRC_PATTERNS = [
'components/**/*.{js,jsx,ts,tsx}',
];

export const DEFAULT_PYTHON_SRC_PATTERNS = ['**/*.py'];
export const DEFAULT_PYTHON_SRC_EXCLUDES = [
'venv/**',
'.venv/**',
'__pycache__/**',
'**/migrations/**',
'**/tests/**',
'**/test_*.py',
'**/*_test.py',
];

/**
* Generates settings from any
* @param flags - The CLI flags to generate settings from
Expand Down Expand Up @@ -158,8 +169,8 @@ export async function generateSettings(
// Add publish if not provided
mergedOptions.publish = (gtConfig.publish || flags.publish) ?? false;

// Populate src if not provided
mergedOptions.src = mergedOptions.src || DEFAULT_SRC_PATTERNS;
// Don't default src here — each pipeline (JS/Python) has its own defaults.
// Only set src if the user explicitly provided it via flags or config.

// Resolve all glob patterns in the files object
const compositePatterns = [
Expand Down
111 changes: 111 additions & 0 deletions packages/cli/src/extraction/__tests__/mapToUpdates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { describe, it, expect } from 'vitest';
import { mapExtractionResultsToUpdates } from '../mapToUpdates.js';
import type { ExtractionResult } from '@generaltranslation/python-extractor';

describe('mapExtractionResultsToUpdates', () => {
it('maps empty results to empty updates', () => {
const updates = mapExtractionResultsToUpdates([]);
expect(updates).toEqual([]);
});

it('maps single result with all metadata fields', () => {
const results: ExtractionResult[] = [
{
dataFormat: 'ICU',
source: 'Hello, {name}!',
metadata: {
id: 'greeting',
context: 'casual',
maxChars: 100,
filePaths: ['app.py'],
staticId: 'static-1',
},
},
];

const updates = mapExtractionResultsToUpdates(results);

expect(updates).toHaveLength(1);
expect(updates[0]).toEqual({
dataFormat: 'ICU',
source: 'Hello, {name}!',
metadata: {
id: 'greeting',
context: 'casual',
maxChars: 100,
filePaths: ['app.py'],
staticId: 'static-1',
},
});
});

it('passes through dataFormat correctly', () => {
const results: ExtractionResult[] = [
{
dataFormat: 'JSX',
source: '<p>Hello</p>',
metadata: {},
},
];

const updates = mapExtractionResultsToUpdates(results);
expect(updates[0].dataFormat).toBe('JSX');
});

it('handles missing optional metadata', () => {
const results: ExtractionResult[] = [
{
dataFormat: 'ICU',
source: 'Simple string',
metadata: {},
},
];

const updates = mapExtractionResultsToUpdates(results);

expect(updates).toHaveLength(1);
expect(updates[0].metadata).toEqual({});
expect(updates[0].metadata.id).toBeUndefined();
expect(updates[0].metadata.context).toBeUndefined();
expect(updates[0].metadata.maxChars).toBeUndefined();
});

it('preserves filePaths array', () => {
const results: ExtractionResult[] = [
{
dataFormat: 'ICU',
source: 'Multi-file string',
metadata: {
filePaths: ['routes/index.py', 'routes/auth.py'],
},
},
];

const updates = mapExtractionResultsToUpdates(results);
expect(updates[0].metadata.filePaths).toEqual([
'routes/index.py',
'routes/auth.py',
]);
});

it('maps multiple results', () => {
const results: ExtractionResult[] = [
{
dataFormat: 'ICU',
source: 'Hello',
metadata: { id: 'hello' },
},
{
dataFormat: 'ICU',
source: 'Goodbye',
metadata: { id: 'goodbye', context: 'farewell' },
},
];

const updates = mapExtractionResultsToUpdates(results);
expect(updates).toHaveLength(2);
expect(updates[0].source).toBe('Hello');
expect(updates[1].source).toBe('Goodbye');
expect(updates[1].metadata.context).toBe('farewell');
});
});
125 changes: 125 additions & 0 deletions packages/cli/src/extraction/__tests__/postProcess.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { describe, it, expect } from 'vitest';
import {
calculateHashes,
dedupeUpdates,
linkStaticUpdates,
} from '../postProcess.js';
import type { Updates } from '../../types/index.js';

describe('calculateHashes', () => {
it('generates consistent hashes for same input', async () => {
const updates1: Updates = [
{ dataFormat: 'ICU', source: 'hello', metadata: {} },
];
const updates2: Updates = [
{ dataFormat: 'ICU', source: 'hello', metadata: {} },
];

await calculateHashes(updates1);
await calculateHashes(updates2);

expect(updates1[0].metadata.hash).toBeDefined();
expect(updates1[0].metadata.hash).toBe(updates2[0].metadata.hash);
});

it('generates different hashes for different sources', async () => {
const updates: Updates = [
{ dataFormat: 'ICU', source: 'hello', metadata: {} },
{ dataFormat: 'ICU', source: 'world', metadata: {} },
];

await calculateHashes(updates);

expect(updates[0].metadata.hash).not.toBe(updates[1].metadata.hash);
});
});

describe('dedupeUpdates', () => {
it('removes duplicates with same hash, merges filePaths', () => {
const updates: Updates = [
{
dataFormat: 'ICU',
source: 'hello',
metadata: { hash: 'h1', filePaths: ['pathA'] },
},
{
dataFormat: 'ICU',
source: 'hello',
metadata: { hash: 'h1', filePaths: ['pathB'] },
},
];

dedupeUpdates(updates);

expect(updates).toHaveLength(1);
expect(updates[0].metadata.filePaths).toEqual(['pathA', 'pathB']);
});

it('keeps distinct entries with different hashes', () => {
const updates: Updates = [
{
dataFormat: 'ICU',
source: 'hello',
metadata: { hash: 'h1', filePaths: ['pathA'] },
},
{
dataFormat: 'ICU',
source: 'world',
metadata: { hash: 'h2', filePaths: ['pathB'] },
},
];

dedupeUpdates(updates);

expect(updates).toHaveLength(2);
});

it('handles entries without hashes', () => {
const updates: Updates = [
{ dataFormat: 'ICU', source: 'no-hash', metadata: {} },
{
dataFormat: 'ICU',
source: 'has-hash',
metadata: { hash: 'h1', filePaths: ['pathA'] },
},
];

dedupeUpdates(updates);

expect(updates).toHaveLength(2);
});
});

describe('linkStaticUpdates', () => {
it('groups entries by temporary staticId and assigns shared hash', () => {
const updates: Updates = [
{
dataFormat: 'ICU',
source: 'variant-a',
metadata: { hash: 'ha', staticId: 'temp-static' },
},
{
dataFormat: 'ICU',
source: 'variant-b',
metadata: { hash: 'hb', staticId: 'temp-static' },
},
];

linkStaticUpdates(updates);

// Both should now share the same staticId (derived from their hashes)
expect(updates[0].metadata.staticId).toBe(updates[1].metadata.staticId);
// The staticId should have been replaced (no longer the temporary value)
expect(updates[0].metadata.staticId).not.toBe('temp-static');
});

it('does not modify entries without staticId', () => {
const updates: Updates = [
{ dataFormat: 'ICU', source: 'no-static', metadata: { hash: 'h1' } },
];

linkStaticUpdates(updates);

expect(updates[0].metadata.staticId).toBeUndefined();
});
});
7 changes: 7 additions & 0 deletions packages/cli/src/extraction/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type { ExtractionResult, ExtractionMetadata } from './types.js';
export { mapExtractionResultsToUpdates } from './mapToUpdates.js';
export {
calculateHashes,
dedupeUpdates,
linkStaticUpdates,
} from './postProcess.js';
25 changes: 25 additions & 0 deletions packages/cli/src/extraction/mapToUpdates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { ExtractionResult } from '@generaltranslation/python-extractor';
import type { Updates } from '../types/index.js';

/**
* Maps ExtractionResult[] to Updates[] format used by the CLI pipeline
*/
export function mapExtractionResultsToUpdates(
results: ExtractionResult[]
): Updates {
return results.map((result) => ({
dataFormat: result.dataFormat,
source: result.source,
metadata: {
...(result.metadata.id && { id: result.metadata.id }),
...(result.metadata.context && { context: result.metadata.context }),
...(result.metadata.maxChars != null && {
maxChars: result.metadata.maxChars,
}),
...(result.metadata.filePaths && {
filePaths: result.metadata.filePaths,
}),
...(result.metadata.staticId && { staticId: result.metadata.staticId }),
},
}));
}
Loading
Loading