-
Notifications
You must be signed in to change notification settings - Fork 662
feat: add --dry-run flag for CLI run command #1421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,277 @@ | ||||||||||
| import chalk from "chalk"; | ||||||||||
| import { Listr } from "listr2"; | ||||||||||
| import _ from "lodash"; | ||||||||||
| import { minimatch } from "minimatch"; | ||||||||||
|
|
||||||||||
| import { colors } from "../../constants"; | ||||||||||
| import { CmdRunContext, CmdRunTask } from "./_types"; | ||||||||||
| import { commonTaskRendererOptions } from "./_const"; | ||||||||||
| import createBucketLoader from "../../loaders"; | ||||||||||
| import { createDeltaProcessor } from "../../utils/delta"; | ||||||||||
|
|
||||||||||
| type DryRunResult = { | ||||||||||
| task: CmdRunTask; | ||||||||||
| sourceCount: number; | ||||||||||
| targetCount: number; | ||||||||||
| added: number; | ||||||||||
| updated: number; | ||||||||||
| renamed: number; | ||||||||||
| removed: number; | ||||||||||
| toTranslate: number; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| export default async function dryRun(input: CmdRunContext) { | ||||||||||
| console.log(chalk.hex(colors.orange)(`[Dry Run - Preview Mode]`)); | ||||||||||
|
|
||||||||||
| const results: DryRunResult[] = []; | ||||||||||
|
|
||||||||||
| await new Listr<CmdRunContext>( | ||||||||||
| [ | ||||||||||
| { | ||||||||||
| title: "Analyzing translation requirements", | ||||||||||
| task: async (ctx, task) => { | ||||||||||
| if (input.tasks.length < 1) { | ||||||||||
| task.title = `No tasks to analyze.`; | ||||||||||
| task.skip(); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const subtasks = ctx.tasks.map((assignedTask) => ({ | ||||||||||
| title: `Analyzing: ${assignedTask.bucketPathPattern.replace( | ||||||||||
| "[locale]", | ||||||||||
| assignedTask.targetLocale, | ||||||||||
| )}`, | ||||||||||
| task: async () => { | ||||||||||
| const bucketLoader = createBucketLoader( | ||||||||||
| assignedTask.bucketType, | ||||||||||
| assignedTask.bucketPathPattern, | ||||||||||
| { | ||||||||||
| defaultLocale: assignedTask.sourceLocale, | ||||||||||
| injectLocale: assignedTask.injectLocale, | ||||||||||
| formatter: assignedTask.formatter, | ||||||||||
| }, | ||||||||||
| assignedTask.lockedKeys, | ||||||||||
| assignedTask.lockedPatterns, | ||||||||||
| assignedTask.ignoredKeys, | ||||||||||
| ); | ||||||||||
| bucketLoader.setDefaultLocale(assignedTask.sourceLocale); | ||||||||||
|
|
||||||||||
| const deltaProcessor = createDeltaProcessor( | ||||||||||
| assignedTask.bucketPathPattern, | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| try { | ||||||||||
| const sourceData = await bucketLoader.pull( | ||||||||||
| assignedTask.sourceLocale, | ||||||||||
| ); | ||||||||||
| const targetData = await bucketLoader.pull( | ||||||||||
| assignedTask.targetLocale, | ||||||||||
| ); | ||||||||||
| const checksums = await deltaProcessor.loadChecksums(); | ||||||||||
| const delta = await deltaProcessor.calculateDelta({ | ||||||||||
| sourceData, | ||||||||||
| targetData, | ||||||||||
| checksums, | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| const processableData = _.chain(sourceData) | ||||||||||
| .entries() | ||||||||||
| .filter( | ||||||||||
| ([key, value]) => | ||||||||||
| delta.added.includes(key) || | ||||||||||
| delta.updated.includes(key) || | ||||||||||
| !!ctx.flags.force, | ||||||||||
| ) | ||||||||||
| .filter( | ||||||||||
| ([key]) => | ||||||||||
| !assignedTask.onlyKeys.length || | ||||||||||
| assignedTask.onlyKeys?.some((pattern) => | ||||||||||
| minimatch(key, pattern), | ||||||||||
| ), | ||||||||||
| ) | ||||||||||
| .fromPairs() | ||||||||||
| .value(); | ||||||||||
|
|
||||||||||
| results.push({ | ||||||||||
| task: assignedTask, | ||||||||||
| sourceCount: Object.keys(sourceData).length, | ||||||||||
| targetCount: Object.keys(targetData).length, | ||||||||||
| added: delta.added.length, | ||||||||||
| updated: delta.updated.length, | ||||||||||
| renamed: delta.renamed.length, | ||||||||||
| removed: delta.removed.length, | ||||||||||
| toTranslate: Object.keys(processableData).length, | ||||||||||
| }); | ||||||||||
| } catch (error: any) { | ||||||||||
| results.push({ | ||||||||||
| task: assignedTask, | ||||||||||
| sourceCount: 0, | ||||||||||
| targetCount: 0, | ||||||||||
| added: 0, | ||||||||||
| updated: 0, | ||||||||||
| renamed: 0, | ||||||||||
| removed: 0, | ||||||||||
| toTranslate: 0, | ||||||||||
| }); | ||||||||||
| } | ||||||||||
| }, | ||||||||||
| })); | ||||||||||
|
|
||||||||||
| return task.newListr(subtasks, { | ||||||||||
| concurrent: true, | ||||||||||
| exitOnError: false, | ||||||||||
| rendererOptions: { | ||||||||||
| ...commonTaskRendererOptions, | ||||||||||
| collapseSubtasks: false, | ||||||||||
| }, | ||||||||||
| }); | ||||||||||
| }, | ||||||||||
| }, | ||||||||||
| ], | ||||||||||
| { | ||||||||||
| exitOnError: false, | ||||||||||
| rendererOptions: commonTaskRendererOptions, | ||||||||||
| }, | ||||||||||
| ).run(input); | ||||||||||
|
|
||||||||||
| // Display summary | ||||||||||
| console.log(); | ||||||||||
| console.log(chalk.hex(colors.orange)("[Dry Run Summary]")); | ||||||||||
| console.log(); | ||||||||||
|
|
||||||||||
| if (results.length === 0) { | ||||||||||
| console.log(chalk.dim("No translation tasks found.")); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Group results by bucket path pattern | ||||||||||
| const groupedResults = _.groupBy( | ||||||||||
| results, | ||||||||||
| (r) => r.task.bucketPathPattern, | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| let totalToTranslate = 0; | ||||||||||
| let totalAdded = 0; | ||||||||||
| let totalUpdated = 0; | ||||||||||
| let totalRenamed = 0; | ||||||||||
|
|
||||||||||
| for (const [pathPattern, taskResults] of Object.entries(groupedResults)) { | ||||||||||
| console.log(chalk.hex(colors.yellow)(`${pathPattern}`)); | ||||||||||
|
|
||||||||||
| for (const result of taskResults) { | ||||||||||
| const displayPath = result.task.bucketPathPattern.replace( | ||||||||||
| "[locale]", | ||||||||||
| result.task.targetLocale, | ||||||||||
| ); | ||||||||||
|
Comment on lines
+162
to
+165
|
||||||||||
| const displayPath = result.task.bucketPathPattern.replace( | |
| "[locale]", | |
| result.task.targetLocale, | |
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import setup from "./setup"; | |
| import plan from "./plan"; | ||
| import execute from "./execute"; | ||
| import watch from "./watch"; | ||
| import dryRun from "./dry-run"; | ||
| import { CmdRunContext, flagsSchema } from "./_types"; | ||
| import frozen from "./frozen"; | ||
| import { | ||
|
|
@@ -117,6 +118,10 @@ export default new Command() | |
| "--sound", | ||
| "Play audio feedback when translations complete (success or failure sounds)", | ||
| ) | ||
| .option( | ||
| "--dry-run", | ||
| "Preview which files would be translated and how many strings would be affected without performing actual translation", | ||
| ) | ||
| .action(async (args) => { | ||
| let authId: string | null = null; | ||
| try { | ||
|
|
@@ -149,14 +154,20 @@ export default new Command() | |
| await plan(ctx); | ||
| await renderSpacer(); | ||
|
|
||
| await frozen(ctx); | ||
| await renderSpacer(); | ||
| // If dry-run mode is enabled, skip execution and show preview | ||
| if (ctx.flags.dryRun) { | ||
| await dryRun(ctx); | ||
| await renderSpacer(); | ||
|
Comment on lines
+157
to
+160
|
||
| } else { | ||
| await frozen(ctx); | ||
| await renderSpacer(); | ||
|
|
||
| await execute(ctx); | ||
| await renderSpacer(); | ||
| await execute(ctx); | ||
| await renderSpacer(); | ||
|
|
||
| await renderSummary(ctx.results); | ||
| await renderSpacer(); | ||
| await renderSummary(ctx.results); | ||
| await renderSpacer(); | ||
| } | ||
|
|
||
| // Play sound after main tasks complete if sound flag is enabled | ||
| if (ctx.flags.sound) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Errors are silently caught and converted to zero-value results, making it difficult to distinguish between a file with no translations and a file that failed to load. Consider logging the error or showing a warning in the output to help users identify configuration or file access issues. For example:
console.warn(chalk.yellow(\⚠ Failed to analyze ${assignedTask.bucketPathPattern}: ${error.message}`))`