Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
78 changes: 78 additions & 0 deletions apps/cli/src/commands/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,59 @@ describe('bundleCommand', () => {
expect(console.log).toHaveBeenCalledWith(' ✅ Locales: en, fr, es');
});

it('should suppress progress and success output in quiet mode', async () => {
await bundleCommand({ name: 'core', quiet: true });

expect(console.log).not.toHaveBeenCalled();
});

it('should display warnings in quiet mode', async () => {
mockGenerateBundle.mockReturnValue({
bundleKey: 'core',
filesGenerated: 3,
warnings: ['Warning 1'],
localesProcessed: ['en', 'fr'],
});

await bundleCommand({ name: 'core', quiet: true });

expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith(' ⚠️ Warnings: 1');
expect(console.log).not.toHaveBeenCalledWith(' - Warning 1');
});

it('should display errors in quiet mode', async () => {
mockGenerateBundle.mockImplementation(() => {
throw new Error('Bundle generation failed');
});

await bundleCommand({ name: 'core', quiet: true });

expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith(' ❌ Bundle generation failed');
});

it('should display type generation errors in quiet mode', async () => {
mockGenerateBundle.mockReturnValue({
bundleKey: 'core',
filesGenerated: 3,
warnings: [],
localesProcessed: ['en'],
typeGenerationResult: {
bundleKey: 'core',
typeDistFile: 'src/generated/core-tokens.ts',
keysCount: 0,
fileGenerated: false,
errorReason: 'Unable to write type file',
},
});

await bundleCommand({ name: 'core', quiet: true });

expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith(' └─ Types: Error (Unable to write type file)');
});

it('should display warnings count when warnings exist', async () => {
mockGenerateBundle.mockReturnValue({
bundleKey: 'core',
Expand Down Expand Up @@ -300,6 +353,31 @@ describe('bundleCommand', () => {
expect(console.log).toHaveBeenCalledWith(' Total warnings: 1');
});

it('should display warning totals in quiet mode for multiple bundles', async () => {
mockGenerateBundle
.mockReturnValueOnce({
bundleKey: 'core',
filesGenerated: 3,
warnings: [],
localesProcessed: ['en', 'fr', 'es'],
})
.mockReturnValueOnce({
bundleKey: 'admin',
filesGenerated: 2,
warnings: ['Warning 1'],
localesProcessed: ['en', 'fr'],
});

await bundleCommand({ name: 'core,admin', quiet: true });

expect(console.log).not.toHaveBeenCalledWith('\n📊 Summary (2 bundles)');
expect(console.log).not.toHaveBeenCalledWith(' Total files generated: 5');
expect(console.log).toHaveBeenCalledWith(' Total warnings: 1');
expect(console.log).toHaveBeenCalledWith(' Run with --verbose to see warning details');
expect(console.log).toHaveBeenCalledWith(' ⚠️ Warnings: 1');
expect(console.log).not.toHaveBeenCalledWith(' - Warning 1');
});

it('should display type generation success', async () => {
mockGenerateBundle.mockReturnValue({
bundleKey: 'core',
Expand Down
53 changes: 33 additions & 20 deletions apps/cli/src/commands/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { loadConfiguration, parseCommaSeparatedList, ConsoleFormatter } from '..
export interface BundleOptions {
name?: string;
locale?: string;
quiet?: boolean;
verbose?: boolean;
/** CLI-level override for token casing. Takes precedence over all config file values. */
tokenCasing?: TokenCasing;
Expand Down Expand Up @@ -92,9 +93,11 @@ export async function bundleCommand(options: BundleOptions): Promise<void> {
continue;
}

console.log('');
ConsoleFormatter.progress(`Generating bundle: ${bundleKey}`);
if (options.verbose && localeFilter) {
if (!options.quiet) {
console.log('');
ConsoleFormatter.progress(`Generating bundle: ${bundleKey}`);
}
if (!options.quiet && options.verbose && localeFilter) {
ConsoleFormatter.indent(`Locales: ${localeFilter.join(', ')}`);
}

Expand All @@ -117,30 +120,36 @@ export async function bundleCommand(options: BundleOptions): Promise<void> {
localesProcessed: result.localesProcessed,
});

ConsoleFormatter.indent(`✅ Files generated: ${result.filesGenerated}`);
ConsoleFormatter.indent(`✅ Locales: ${result.localesProcessed.join(', ')}`);
if (!options.quiet) {
ConsoleFormatter.indent(`✅ Files generated: ${result.filesGenerated}`);
ConsoleFormatter.indent(`✅ Locales: ${result.localesProcessed.join(', ')}`);
}

if (result.typeGenerationResult) {
if (result.typeGenerationResult.fileGenerated) {
ConsoleFormatter.indent(
`└─ Types: ${result.typeGenerationResult.typeDistFile} (${result.typeGenerationResult.keysCount} keys)`,
);
if (!options.quiet) {
ConsoleFormatter.indent(
`└─ Types: ${result.typeGenerationResult.typeDistFile} (${result.typeGenerationResult.keysCount} keys)`,
);
}
} else if (result.typeGenerationResult.errorReason) {
ConsoleFormatter.indent(`└─ Types: Error (${result.typeGenerationResult.errorReason})`);
} else if (result.typeGenerationResult.skippedReason) {
const skippedReasonMessages: Record<string, string> = {
'empty-bundle': 'bundle has no keys',
'not-configured': 'no typeDistFile configured',
};
const skippedMessage =
skippedReasonMessages[result.typeGenerationResult.skippedReason] ??
result.typeGenerationResult.skippedReason;
ConsoleFormatter.indent(`└─ Types: Skipped (${skippedMessage})`);
if (!options.quiet) {
const skippedReasonMessages: Record<string, string> = {
'empty-bundle': 'bundle has no keys',
'not-configured': 'no typeDistFile configured',
};
const skippedMessage =
skippedReasonMessages[result.typeGenerationResult.skippedReason] ??
result.typeGenerationResult.skippedReason;
ConsoleFormatter.indent(`└─ Types: Skipped (${skippedMessage})`);
}
}
} else if (hasTypeDistConfigured(bundleDefinition)) {
// Should have result if configured, but just in case
ConsoleFormatter.indent(`└─ Types: Failed (No result returned)`);
} else {
} else if (!options.quiet) {
ConsoleFormatter.indent(`└─ Types: Skipped (no typeDistFile configured)`);
}

Expand Down Expand Up @@ -182,8 +191,10 @@ export async function bundleCommand(options: BundleOptions): Promise<void> {
},
);

ConsoleFormatter.section(`Summary (${totals.bundlesProcessed} bundles)`);
ConsoleFormatter.keyValue('Total files generated', totals.filesGenerated);
if (!options.quiet) {
ConsoleFormatter.section(`Summary (${totals.bundlesProcessed} bundles)`);
ConsoleFormatter.keyValue('Total files generated', totals.filesGenerated);
}

if (totals.warningsCount > 0) {
ConsoleFormatter.keyValue('Total warnings', totals.warningsCount);
Expand All @@ -194,7 +205,9 @@ export async function bundleCommand(options: BundleOptions): Promise<void> {

const errors = bundleResults.filter((r) => r.error);
if (errors.length > 0) {
console.log('');
if (!options.quiet) {
console.log('');
}
ConsoleFormatter.warning(`${errors.length} bundle(s) failed to generate`);
}
}
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ program
.description('Generate translation bundles for deployment')
.option('--name <names>', 'Bundle name(s) - single name or comma-separated (e.g., core,admin)')
.option('--locale <locales>', 'Locale(s) to generate - comma-separated (e.g., en,fr)')
.option('--quiet', 'Suppress progress and success output (warnings and errors are still shown)')
.option('--verbose', 'Show detailed output including warnings')
.addOption(new Option('--token-casing <casing>', 'Token property key casing').choices(['upperCase', 'camelCase']))
.option(
Expand Down
1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,7 @@ lingo-tracker bundle [options]
- `--token-constant-name <name>` - Custom name for the generated TypeScript constant. Must be a valid JavaScript identifier. Only works when targeting a single bundle (via `--name`). Overrides `tokenConstantName` in the bundle config
- `--no-transform-icu-to-transloco` - Disable ICU to Transloco format conversion in bundle output
- `--debug-keys [locale]` - Also emit a debug bundle where every value equals its own dot-delimited key. Useful for visually locating untranslated strings at runtime by switching your app to this locale. If no locale code is provided, defaults to `99` (e.g. `main.99.json`). ICU transformation is skipped for the debug bundle.
- `--quiet` - Suppress progress and success output (warnings and errors are still shown)
- `--verbose` - Show detailed output including all warnings

**What Bundle Generation Does:**
Expand Down