Skip to content

Commit 6bc309c

Browse files
feat(cli): made i18n command more robust (#274)
* feat(cli): made i18n command more robust Signed-off-by: Partik <[email protected]> * Delete pnpm-lock.yaml * fix: pnpm-lock.yaml change Signed-off-by: Partik <[email protected]> * fix: changesets added Signed-off-by: Partik <[email protected]> * fix: revert the lock file Signed-off-by: Partik <[email protected]> * fix: revert the lock file Signed-off-by: Partik <[email protected]> * fix: revert the lock file Signed-off-by: Partik <[email protected]> --------- Signed-off-by: Partik <[email protected]> Co-authored-by: Max Prilutskiy <[email protected]>
1 parent dc66138 commit 6bc309c

File tree

4 files changed

+128
-87
lines changed

4 files changed

+128
-87
lines changed

.changeset/fair-mice-shave.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

.changeset/nervous-bugs-remain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@replexica/cli": minor
3+
---
4+
5+
Improved error handling for i18n command

packages/cli/src/cli/i18n.ts

Lines changed: 120 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { bucketTypeSchema, I18nConfig, localeCodeSchema } from '@replexica/spec';
22
import { ReplexicaEngine } from '@replexica/sdk';
33
import { Command } from 'commander';
4-
import Z from 'zod';
4+
import Z, { any } from 'zod';
55
import _ from 'lodash';
66
import { getConfig } from '../utils/config';
77
import { getSettings } from '../utils/settings';
@@ -22,117 +22,134 @@ export default new Command()
2222
.option('--force', 'Ignore lockfile and process all keys')
2323
.option('--verbose', 'Show verbose output')
2424
.option('--api-key <api-key>', 'Explicitly set the API key to use')
25+
.option('--strict', 'Stop on first error')
2526
.action(async function (options) {
2627
const ora = Ora();
27-
28+
const results: any = [];
29+
const flags = parseFlags(options);
30+
2831
try {
2932
ora.start('Loading configuration...');
30-
const flags = parseFlags(options);
3133
const i18nConfig = getConfig();
3234
const settings = getSettings(flags.apiKey);
3335
ora.succeed('Configuration loaded');
36+
37+
try {
38+
ora.start('Validating localization configuration...');
39+
validateParams(i18nConfig, flags);
40+
ora.succeed('Localization configuration is valid');
41+
} catch (error:any) {
42+
handleWarning('Localization configuration validation failed', error, true, results);
43+
return;
44+
}
3445

35-
ora.start('Validating localization configuration...');
36-
validateParams(i18nConfig, flags);
37-
ora.succeed('Localization configuration is valid');
38-
39-
ora.start('Connecting to Replexica Localization Engine...');
40-
const auth = await validateAuth(settings);
41-
ora.succeed('Replexica Localization Engine connected');
42-
ora.succeed(`Authenticated as ${auth.email}`);
46+
try {
47+
ora.start('Connecting to Replexica Localization Engine...');
48+
const auth = await validateAuth(settings);
49+
ora.succeed(`Authenticated as ${auth.email}`);
50+
} catch (error:any) {
51+
handleWarning('Failed to connect to Replexica Localization Engine', error, true, results);
52+
return;
53+
}
4354

44-
let buckets = getBuckets(i18nConfig!);
45-
if (flags.bucket) {
46-
buckets = buckets.filter((bucket) => bucket.type === flags.bucket);
55+
let buckets:any = [];
56+
try {
57+
buckets = getBuckets(i18nConfig!);
58+
if (flags.bucket) {
59+
buckets = buckets.filter((bucket:any) => bucket.type === flags.bucket);
60+
}
61+
ora.succeed('Buckets retrieved');
62+
} catch (error:any) {
63+
handleWarning('Failed to retrieve buckets', error, true, results);
64+
return;
4765
}
4866

4967
const targetLocales = getTargetLocales(i18nConfig!, flags);
5068
const lockfileHelper = createLockfileHelper();
5169

5270
// Ensure the lockfile exists
53-
ora.start('Ensuring i18n.lock exists...');
54-
if (!lockfileHelper.isLockfileExists()) {
55-
ora.start('Creating i18n.lock...');
56-
for (const bucket of buckets) {
57-
for (const pathPattern of bucket.pathPatterns) {
58-
const bucketLoader = createBucketLoader(bucket.type, pathPattern);
59-
bucketLoader.setDefaultLocale(i18nConfig!.locale.source);
60-
61-
const sourceData = await bucketLoader.pull(i18nConfig!.locale.source);
62-
lockfileHelper.registerSourceData(pathPattern, sourceData);
71+
try {
72+
ora.start('Ensuring i18n.lock exists...');
73+
if (!lockfileHelper.isLockfileExists()) {
74+
ora.start('Creating i18n.lock...');
75+
for (const bucket of buckets) {
76+
for (const pathPattern of bucket.pathPatterns) {
77+
const bucketLoader = createBucketLoader(bucket.type, pathPattern);
78+
bucketLoader.setDefaultLocale(i18nConfig!.locale.source);
79+
80+
const sourceData = await bucketLoader.pull(i18nConfig!.locale.source);
81+
lockfileHelper.registerSourceData(pathPattern, sourceData);
82+
}
6383
}
84+
ora.succeed('i18n.lock created');
85+
} else {
86+
ora.succeed('i18n.lock loaded');
6487
}
65-
ora.succeed('i18n.lock created');
66-
} else {
67-
ora.succeed('i18n.lock loaded');
88+
} catch (error:any) {
89+
handleWarning('Failed to ensure i18n.lock existence', error, true, results);
90+
return;
6891
}
6992

70-
// Exit with error if frozen flag is provided and there are any updated keys
71-
if (flags.frozen) {
72-
for (const bucket of buckets) {
93+
// Process each bucket
94+
for (const bucket of buckets) {
95+
try {
96+
console.log();
97+
ora.info(`Processing bucket: ${bucket.type}`);
7398
for (const pathPattern of bucket.pathPatterns) {
99+
const bucketOra = Ora({ indent: 2 }).info(`Processing path: ${pathPattern}`);
74100
const bucketLoader = createBucketLoader(bucket.type, pathPattern);
75101
bucketLoader.setDefaultLocale(i18nConfig!.locale.source);
76102

77103
const sourceData = await bucketLoader.pull(i18nConfig!.locale.source);
78-
const updatedSourceData = lockfileHelper.extractUpdatedData(pathPattern, sourceData);
79-
if (Object.keys(updatedSourceData).length) {
80-
throw new ReplexicaCLIError({
81-
message: `Translations are not up to date. Run the command without the --frozen flag to update the translations, then try again.`,
82-
docUrl: "translationFailed"
83-
});
84-
}
85-
}
86-
}
87-
}
88-
// Process each bucket
89-
for (const bucket of buckets) {
90-
console.log();
91-
ora.info(`Processing bucket: ${bucket.type}`);
92-
for (const pathPattern of bucket.pathPatterns) {
93-
const bucketOra = Ora({ indent: 2 }).info(`Processing path: ${pathPattern}`);
94-
95-
const bucketLoader = createBucketLoader(bucket.type, pathPattern);
96-
bucketLoader.setDefaultLocale(i18nConfig!.locale.source);
97-
98-
const sourceData = await bucketLoader.pull(i18nConfig!.locale.source);
99-
const updatedSourceData = flags.force ? sourceData : lockfileHelper.extractUpdatedData(pathPattern, sourceData);
100-
101-
for (const targetLocale of targetLocales) {
102-
bucketOra.start(`[${i18nConfig!.locale.source} -> ${targetLocale}] AI localization in progress...`);
103-
104-
const targetData = await bucketLoader.pull(targetLocale);
105-
106-
const processableData = calculateDataDelta({ sourceData, updatedSourceData, targetData });
107-
if (flags.verbose) {
108-
bucketOra.info(JSON.stringify(processableData, null, 2));
109-
}
110-
111-
const localizationEngine = createLocalizationEngineConnection({
112-
apiKey: settings.auth.apiKey,
113-
apiUrl: settings.auth.apiUrl,
114-
});
115-
const processedTargetData = await localizationEngine.process({
116-
sourceLocale: i18nConfig!.locale.source,
117-
sourceData,
118-
processableData,
119-
targetLocale,
120-
targetData,
121-
}, (progress) => {
122-
bucketOra.text = `[${i18nConfig!.locale.source} -> ${targetLocale}] (${progress}%) AI localization in progress...`;
123-
});
124-
125-
if (flags.verbose) {
126-
bucketOra.info(JSON.stringify(processedTargetData, null, 2));
104+
const updatedSourceData = flags.force ? sourceData : lockfileHelper.extractUpdatedData(pathPattern, sourceData);
105+
106+
for (const targetLocale of targetLocales) {
107+
try {
108+
bucketOra.start(`[${i18nConfig!.locale.source} -> ${targetLocale}] AI localization in progress...`);
109+
110+
const targetData = await bucketLoader.pull(targetLocale);
111+
const processableData = calculateDataDelta({ sourceData, updatedSourceData, targetData });
112+
if (flags.verbose) {
113+
bucketOra.info(JSON.stringify(processableData, null, 2));
114+
}
115+
116+
const localizationEngine = createLocalizationEngineConnection({
117+
apiKey: settings.auth.apiKey,
118+
apiUrl: settings.auth.apiUrl,
119+
});
120+
let processedTargetData;
121+
try {
122+
processedTargetData = await localizationEngine.process({
123+
sourceLocale: i18nConfig!.locale.source,
124+
sourceData,
125+
processableData,
126+
targetLocale,
127+
targetData,
128+
}, (progress) => {
129+
bucketOra.text = `[${i18nConfig!.locale.source} -> ${targetLocale}] (${progress}%) AI localization in progress...`;
130+
});
131+
132+
} catch (error: any) {
133+
handleWarning('Failed to process target data', error, flags.strict, results);
134+
if (flags.strict) return;
135+
}
136+
137+
if (flags.verbose) {
138+
bucketOra.info(JSON.stringify(processedTargetData, null, 2));
139+
}
140+
const finalTargetData = _.merge({}, sourceData, targetData, processedTargetData);
141+
await bucketLoader.push(targetLocale, finalTargetData);
142+
bucketOra.succeed(`[${i18nConfig!.locale.source} -> ${targetLocale}] AI localization completed`);
143+
} catch (error:any) {
144+
handleWarning(`Failed to localize for ${targetLocale}`, error, flags.strict, results);
145+
if (flags.strict) return;
146+
}
127147
}
128-
const finalTargetData = _.merge({}, sourceData, targetData, processedTargetData);
129-
130-
await bucketLoader.push(targetLocale, finalTargetData);
131-
132-
bucketOra.succeed(`[${i18nConfig!.locale.source} -> ${targetLocale}] AI localization completed`);
148+
lockfileHelper.registerSourceData(pathPattern, sourceData);
133149
}
134-
135-
lockfileHelper.registerSourceData(pathPattern, sourceData);
150+
} catch (error:any) {
151+
handleWarning(`Failed to process bucket: ${bucket.type}`, error, flags.strict, results);
152+
if (flags.strict) return;
136153
}
137154
}
138155

@@ -141,10 +158,26 @@ export default new Command()
141158
} catch (error: any) {
142159
ora.fail(error.message);
143160
process.exit(1);
161+
} finally {
162+
displaySummary(results);
144163
}
145164
});
146165

147166

167+
function handleWarning(step: string, error: Error, strictMode: boolean| undefined, results: any[]) {
168+
console.warn(`[WARNING] ${step}: ${error.message}`);
169+
results.push({ step, status: "Failed", error: error.message });
170+
if (strictMode) throw error;
171+
}
172+
173+
function displaySummary(results: any[]) {
174+
console.log("\nProcess Summary:");
175+
results.forEach((result) => {
176+
console.log(`${result.step}: ${result.status}`);
177+
if (result.error) console.log(` - Error: ${result.error}`);
178+
});
179+
}
180+
148181
function calculateDataDelta(args: {
149182
sourceData: Record<string, any>;
150183
updatedSourceData: Record<string, any>;
@@ -210,6 +243,7 @@ function parseFlags(options: any) {
210243
force: Z.boolean().optional(),
211244
frozen: Z.boolean().optional(),
212245
verbose: Z.boolean().optional(),
246+
strict: Z.boolean().optional(),
213247
}).parse(options);
214248
}
215249

pnpm-lock.yaml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)