lingo.dev 0.84.0 → 0.85.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.
package/build/cli.mjs CHANGED
@@ -5111,10 +5111,424 @@ var ci_default = new Command10().command("ci").description("Run Lingo.dev CI/CD
5111
5111
  main();
5112
5112
  });
5113
5113
 
5114
+ // src/cli/cmd/status.ts
5115
+ import { bucketTypeSchema as bucketTypeSchema3, localeCodeSchema as localeCodeSchema2, resolveOverriddenLocale as resolveOverriddenLocale6 } from "@lingo.dev/_spec";
5116
+ import { Command as Command11 } from "interactive-commander";
5117
+ import Z11 from "zod";
5118
+ import Ora8 from "ora";
5119
+ import chalk2 from "chalk";
5120
+ import Table from "cli-table3";
5121
+ var status_default = new Command11().command("status").description("Show the status of the localization process").helpOption("-h, --help", "Show help").option("--locale <locale>", "Locale to process", (val, prev) => prev ? [...prev, val] : [val]).option("--bucket <bucket>", "Bucket to process", (val, prev) => prev ? [...prev, val] : [val]).option(
5122
+ "--file [files...]",
5123
+ "File to process. Process only a specific path, may contain asterisk * to match multiple files."
5124
+ ).option("--force", "Ignore lockfile and process all keys, useful for estimating full re-translation").option("--verbose", "Show detailed output including key-level word counts").option("--api-key <api-key>", "Explicitly set the API key to use, override the default API key from settings").action(async function(options) {
5125
+ const ora = Ora8();
5126
+ const flags = parseFlags2(options);
5127
+ let authId = null;
5128
+ try {
5129
+ ora.start("Loading configuration...");
5130
+ const i18nConfig = getConfig();
5131
+ const settings = getSettings(flags.apiKey);
5132
+ ora.succeed("Configuration loaded");
5133
+ try {
5134
+ ora.start("Checking authentication status...");
5135
+ const auth = await tryAuthenticate(settings);
5136
+ if (auth) {
5137
+ authId = auth.id;
5138
+ ora.succeed(`Authenticated as ${auth.email}`);
5139
+ } else {
5140
+ ora.info(
5141
+ "Not authenticated. Continuing without authentication. (Run `lingo.dev auth --login` to authenticate)"
5142
+ );
5143
+ }
5144
+ } catch (error) {
5145
+ ora.info("Authentication failed. Continuing without authentication.");
5146
+ }
5147
+ ora.start("Validating localization configuration...");
5148
+ validateParams2(i18nConfig, flags);
5149
+ ora.succeed("Localization configuration is valid");
5150
+ trackEvent(authId || "status", "cmd.status.start", {
5151
+ i18nConfig,
5152
+ flags
5153
+ });
5154
+ let buckets = getBuckets(i18nConfig);
5155
+ if (flags.bucket?.length) {
5156
+ buckets = buckets.filter((bucket) => flags.bucket.includes(bucket.type));
5157
+ }
5158
+ ora.succeed("Buckets retrieved");
5159
+ if (flags.file?.length) {
5160
+ buckets = buckets.map((bucket) => {
5161
+ const paths = bucket.paths.filter((path18) => flags.file.find((file) => path18.pathPattern?.match(file)));
5162
+ return { ...bucket, paths };
5163
+ }).filter((bucket) => bucket.paths.length > 0);
5164
+ if (buckets.length === 0) {
5165
+ ora.fail("No buckets found. All buckets were filtered out by --file option.");
5166
+ process.exit(1);
5167
+ } else {
5168
+ ora.info(`\x1B[36mProcessing only filtered buckets:\x1B[0m`);
5169
+ buckets.map((bucket) => {
5170
+ ora.info(` ${bucket.type}:`);
5171
+ bucket.paths.forEach((path18) => {
5172
+ ora.info(` - ${path18.pathPattern}`);
5173
+ });
5174
+ });
5175
+ }
5176
+ }
5177
+ const targetLocales = flags.locale?.length ? flags.locale : i18nConfig.locale.targets;
5178
+ let totalSourceKeyCount = 0;
5179
+ let uniqueKeysToTranslate = 0;
5180
+ let totalExistingTranslations = 0;
5181
+ const totalWordCount = /* @__PURE__ */ new Map();
5182
+ const languageStats = {};
5183
+ for (const locale of targetLocales) {
5184
+ languageStats[locale] = {
5185
+ complete: 0,
5186
+ missing: 0,
5187
+ updated: 0,
5188
+ words: 0
5189
+ };
5190
+ totalWordCount.set(locale, 0);
5191
+ }
5192
+ const fileStats = {};
5193
+ for (const bucket of buckets) {
5194
+ try {
5195
+ console.log();
5196
+ ora.info(`Analyzing bucket: ${bucket.type}`);
5197
+ for (const bucketPath of bucket.paths) {
5198
+ const bucketOra = Ora8({ indent: 2 }).info(`Analyzing path: ${bucketPath.pathPattern}`);
5199
+ const sourceLocale = resolveOverriddenLocale6(i18nConfig.locale.source, bucketPath.delimiter);
5200
+ const bucketLoader = createBucketLoader(
5201
+ bucket.type,
5202
+ bucketPath.pathPattern,
5203
+ {
5204
+ isCacheRestore: false,
5205
+ defaultLocale: sourceLocale,
5206
+ injectLocale: bucket.injectLocale
5207
+ },
5208
+ bucket.lockedKeys
5209
+ );
5210
+ bucketLoader.setDefaultLocale(sourceLocale);
5211
+ await bucketLoader.init();
5212
+ const filePath = bucketPath.pathPattern;
5213
+ if (!fileStats[filePath]) {
5214
+ fileStats[filePath] = {
5215
+ path: filePath,
5216
+ sourceKeys: 0,
5217
+ wordCount: 0,
5218
+ languageStats: {}
5219
+ };
5220
+ for (const locale of targetLocales) {
5221
+ fileStats[filePath].languageStats[locale] = {
5222
+ complete: 0,
5223
+ missing: 0,
5224
+ updated: 0,
5225
+ words: 0
5226
+ };
5227
+ }
5228
+ }
5229
+ const sourceData = await bucketLoader.pull(sourceLocale);
5230
+ const sourceKeys = Object.keys(sourceData);
5231
+ fileStats[filePath].sourceKeys = sourceKeys.length;
5232
+ totalSourceKeyCount += sourceKeys.length;
5233
+ let sourceWordCount = 0;
5234
+ for (const key of sourceKeys) {
5235
+ const value = sourceData[key];
5236
+ if (typeof value === "string") {
5237
+ const words = value.trim().split(/\s+/).length;
5238
+ sourceWordCount += words;
5239
+ }
5240
+ }
5241
+ fileStats[filePath].wordCount = sourceWordCount;
5242
+ for (const _targetLocale of targetLocales) {
5243
+ const targetLocale = resolveOverriddenLocale6(_targetLocale, bucketPath.delimiter);
5244
+ bucketOra.start(`[${sourceLocale} -> ${targetLocale}] Analyzing translation status...`);
5245
+ let targetData = {};
5246
+ let fileExists = true;
5247
+ try {
5248
+ targetData = await bucketLoader.pull(targetLocale);
5249
+ } catch (error) {
5250
+ fileExists = false;
5251
+ bucketOra.info(
5252
+ `[${sourceLocale} -> ${targetLocale}] Target file not found, assuming all keys need translation.`
5253
+ );
5254
+ }
5255
+ if (!fileExists) {
5256
+ fileStats[filePath].languageStats[targetLocale].missing = sourceKeys.length;
5257
+ fileStats[filePath].languageStats[targetLocale].words = sourceWordCount;
5258
+ languageStats[targetLocale].missing += sourceKeys.length;
5259
+ languageStats[targetLocale].words += sourceWordCount;
5260
+ totalWordCount.set(targetLocale, (totalWordCount.get(targetLocale) || 0) + sourceWordCount);
5261
+ bucketOra.succeed(
5262
+ `[${sourceLocale} -> ${targetLocale}] ${chalk2.red(`0% complete`)} (0/${sourceKeys.length} keys) - file not found`
5263
+ );
5264
+ continue;
5265
+ }
5266
+ const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
5267
+ const checksums = await deltaProcessor.loadChecksums();
5268
+ const delta = await deltaProcessor.calculateDelta({
5269
+ sourceData,
5270
+ targetData,
5271
+ checksums
5272
+ });
5273
+ const missingKeys = delta.added;
5274
+ const updatedKeys = delta.updated;
5275
+ const completeKeys = sourceKeys.filter((key) => !missingKeys.includes(key) && !updatedKeys.includes(key));
5276
+ let wordsToTranslate = 0;
5277
+ const keysToProcess = flags.force ? sourceKeys : [...missingKeys, ...updatedKeys];
5278
+ for (const key of keysToProcess) {
5279
+ const value = sourceData[String(key)];
5280
+ if (typeof value === "string") {
5281
+ const words = value.trim().split(/\s+/).length;
5282
+ wordsToTranslate += words;
5283
+ }
5284
+ }
5285
+ fileStats[filePath].languageStats[targetLocale].missing = missingKeys.length;
5286
+ fileStats[filePath].languageStats[targetLocale].updated = updatedKeys.length;
5287
+ fileStats[filePath].languageStats[targetLocale].complete = completeKeys.length;
5288
+ fileStats[filePath].languageStats[targetLocale].words = wordsToTranslate;
5289
+ languageStats[targetLocale].missing += missingKeys.length;
5290
+ languageStats[targetLocale].updated += updatedKeys.length;
5291
+ languageStats[targetLocale].complete += completeKeys.length;
5292
+ languageStats[targetLocale].words += wordsToTranslate;
5293
+ totalWordCount.set(targetLocale, (totalWordCount.get(targetLocale) || 0) + wordsToTranslate);
5294
+ const totalKeysInFile = sourceKeys.length;
5295
+ const completionPercent = (completeKeys.length / totalKeysInFile * 100).toFixed(1);
5296
+ if (missingKeys.length === 0 && updatedKeys.length === 0) {
5297
+ bucketOra.succeed(
5298
+ `[${sourceLocale} -> ${targetLocale}] ${chalk2.green(`100% complete`)} (${completeKeys.length}/${totalKeysInFile} keys)`
5299
+ );
5300
+ } else {
5301
+ const message = `[${sourceLocale} -> ${targetLocale}] ${parseFloat(completionPercent) > 50 ? chalk2.yellow(`${completionPercent}% complete`) : chalk2.red(`${completionPercent}% complete`)} (${completeKeys.length}/${totalKeysInFile} keys)`;
5302
+ bucketOra.succeed(message);
5303
+ if (flags.verbose) {
5304
+ if (missingKeys.length > 0) {
5305
+ console.log(` ${chalk2.red(`Missing:`)} ${missingKeys.length} keys, ~${wordsToTranslate} words`);
5306
+ console.log(
5307
+ ` ${chalk2.dim(`Example missing: ${missingKeys.slice(0, 2).join(", ")}${missingKeys.length > 2 ? "..." : ""}`)}`
5308
+ );
5309
+ }
5310
+ if (updatedKeys.length > 0) {
5311
+ console.log(` ${chalk2.yellow(`Updated:`)} ${updatedKeys.length} keys that changed in source`);
5312
+ }
5313
+ }
5314
+ }
5315
+ }
5316
+ }
5317
+ } catch (error) {
5318
+ ora.fail(`Failed to analyze bucket ${bucket.type}: ${error.message}`);
5319
+ }
5320
+ }
5321
+ const totalKeysNeedingTranslation = Object.values(languageStats).reduce((sum, stats) => {
5322
+ return sum + stats.missing + stats.updated;
5323
+ }, 0);
5324
+ const totalCompletedKeys = totalSourceKeyCount - totalKeysNeedingTranslation / targetLocales.length;
5325
+ console.log();
5326
+ ora.succeed(chalk2.green(`Localization status completed.`));
5327
+ console.log(chalk2.bold.cyan(`
5328
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557`));
5329
+ console.log(chalk2.bold.cyan(`\u2551 LOCALIZATION STATUS REPORT \u2551`));
5330
+ console.log(chalk2.bold.cyan(`\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D`));
5331
+ console.log(chalk2.bold(`
5332
+ \u{1F4DD} SOURCE CONTENT:`));
5333
+ console.log(`\u2022 Source language: ${chalk2.green(i18nConfig.locale.source)}`);
5334
+ console.log(`\u2022 Source keys: ${chalk2.yellow(totalSourceKeyCount.toString())} keys across all files`);
5335
+ console.log(chalk2.bold(`
5336
+ \u{1F310} LANGUAGE BY LANGUAGE BREAKDOWN:`));
5337
+ const table = new Table({
5338
+ head: ["Language", "Status", "Complete", "Missing", "Updated", "Words"],
5339
+ style: {
5340
+ head: ["white"],
5341
+ // White color for headers
5342
+ border: []
5343
+ // No color for borders
5344
+ },
5345
+ colWidths: [12, 20, 18, 12, 12, 15]
5346
+ // Explicit column widths, making Status column wider
5347
+ });
5348
+ let totalWordsToTranslate = 0;
5349
+ for (const locale of targetLocales) {
5350
+ const stats = languageStats[locale];
5351
+ const percentComplete = (stats.complete / totalSourceKeyCount * 100).toFixed(1);
5352
+ let statusText;
5353
+ let statusColor;
5354
+ if (stats.missing === totalSourceKeyCount) {
5355
+ statusText = "\u{1F534} Not started";
5356
+ statusColor = chalk2.red;
5357
+ } else if (stats.missing === 0 && stats.updated === 0) {
5358
+ statusText = "\u2705 Complete";
5359
+ statusColor = chalk2.green;
5360
+ } else if (parseFloat(percentComplete) > 80) {
5361
+ statusText = "\u{1F7E1} Almost done";
5362
+ statusColor = chalk2.yellow;
5363
+ } else if (parseFloat(percentComplete) > 0) {
5364
+ statusText = "\u{1F7E0} In progress";
5365
+ statusColor = chalk2.yellow;
5366
+ } else {
5367
+ statusText = "\u{1F534} Not started";
5368
+ statusColor = chalk2.red;
5369
+ }
5370
+ const words = totalWordCount.get(locale) || 0;
5371
+ totalWordsToTranslate += words;
5372
+ table.push([
5373
+ locale,
5374
+ statusColor(statusText),
5375
+ `${stats.complete}/${totalSourceKeyCount} (${percentComplete}%)`,
5376
+ stats.missing > 0 ? chalk2.red(stats.missing.toString()) : "0",
5377
+ stats.updated > 0 ? chalk2.yellow(stats.updated.toString()) : "0",
5378
+ words > 0 ? `~${words.toLocaleString()}` : "0"
5379
+ ]);
5380
+ }
5381
+ console.log(table.toString());
5382
+ console.log(chalk2.bold(`
5383
+ \u{1F4CA} USAGE ESTIMATE:`));
5384
+ console.log(
5385
+ `\u2022 TOTAL: ~${chalk2.yellow.bold(totalWordsToTranslate.toLocaleString())} words to translate across all languages`
5386
+ );
5387
+ if (targetLocales.length > 1) {
5388
+ console.log(`\u2022 Per-language breakdown:`);
5389
+ for (const locale of targetLocales) {
5390
+ const words = totalWordCount.get(locale) || 0;
5391
+ const percent = (words / totalWordsToTranslate * 100).toFixed(1);
5392
+ console.log(` - ${locale}: ~${words.toLocaleString()} words (${percent}% of total)`);
5393
+ }
5394
+ }
5395
+ if (flags.confirm && Object.keys(fileStats).length > 0) {
5396
+ console.log(chalk2.bold(`
5397
+ \u{1F4D1} BREAKDOWN BY FILE:`));
5398
+ Object.entries(fileStats).sort((a, b) => b[1].wordCount - a[1].wordCount).forEach(([path18, stats]) => {
5399
+ if (stats.sourceKeys === 0) return;
5400
+ console.log(chalk2.bold(`
5401
+ \u2022 ${path18}:`));
5402
+ console.log(` ${stats.sourceKeys} source keys, ~${stats.wordCount.toLocaleString()} source words`);
5403
+ const fileTable = new Table({
5404
+ head: ["Language", "Status", "Details"],
5405
+ style: {
5406
+ head: ["white"],
5407
+ border: []
5408
+ },
5409
+ colWidths: [12, 20, 50]
5410
+ // Explicit column widths for file detail table
5411
+ });
5412
+ for (const locale of targetLocales) {
5413
+ const langStats = stats.languageStats[locale];
5414
+ const complete = langStats.complete;
5415
+ const total = stats.sourceKeys;
5416
+ const completion = (complete / total * 100).toFixed(1);
5417
+ let status = "\u2705 Complete";
5418
+ let statusColor = chalk2.green;
5419
+ if (langStats.missing === total) {
5420
+ status = "\u274C Not started";
5421
+ statusColor = chalk2.red;
5422
+ } else if (langStats.missing > 0 || langStats.updated > 0) {
5423
+ status = `\u26A0\uFE0F ${completion}% complete`;
5424
+ statusColor = chalk2.yellow;
5425
+ }
5426
+ let details = "";
5427
+ if (langStats.missing > 0 || langStats.updated > 0) {
5428
+ const parts = [];
5429
+ if (langStats.missing > 0) parts.push(`${langStats.missing} missing`);
5430
+ if (langStats.updated > 0) parts.push(`${langStats.updated} changed`);
5431
+ details = `${parts.join(", ")}, ~${langStats.words} words`;
5432
+ } else {
5433
+ details = "All keys translated";
5434
+ }
5435
+ fileTable.push([locale, statusColor(status), details]);
5436
+ }
5437
+ console.log(fileTable.toString());
5438
+ });
5439
+ }
5440
+ const completeLanguages = targetLocales.filter(
5441
+ (locale) => languageStats[locale].missing === 0 && languageStats[locale].updated === 0
5442
+ );
5443
+ const missingLanguages = targetLocales.filter((locale) => languageStats[locale].complete === 0);
5444
+ console.log(chalk2.bold.green(`
5445
+ \u{1F4A1} OPTIMIZATION TIPS:`));
5446
+ if (missingLanguages.length > 0) {
5447
+ console.log(
5448
+ `\u2022 ${chalk2.yellow(missingLanguages.join(", "))} ${missingLanguages.length === 1 ? "has" : "have"} no translations yet`
5449
+ );
5450
+ }
5451
+ if (completeLanguages.length > 0) {
5452
+ console.log(
5453
+ `\u2022 ${chalk2.green(completeLanguages.join(", "))} ${completeLanguages.length === 1 ? "is" : "are"} completely translated`
5454
+ );
5455
+ }
5456
+ if (targetLocales.length > 1) {
5457
+ console.log(`\u2022 Translating one language at a time reduces complexity`);
5458
+ console.log(`\u2022 Try 'lingo.dev@latest i18n --locale ${targetLocales[0]}' to process just one language`);
5459
+ }
5460
+ trackEvent(authId || "status", "cmd.status.success", {
5461
+ i18nConfig,
5462
+ flags,
5463
+ totalSourceKeyCount,
5464
+ languageStats,
5465
+ totalWordsToTranslate,
5466
+ authenticated: !!authId
5467
+ });
5468
+ } catch (error) {
5469
+ ora.fail(error.message);
5470
+ trackEvent(authId || "status", "cmd.status.error", {
5471
+ flags,
5472
+ error: error.message,
5473
+ authenticated: !!authId
5474
+ });
5475
+ process.exit(1);
5476
+ }
5477
+ });
5478
+ function parseFlags2(options) {
5479
+ return Z11.object({
5480
+ locale: Z11.array(localeCodeSchema2).optional(),
5481
+ bucket: Z11.array(bucketTypeSchema3).optional(),
5482
+ force: Z11.boolean().optional(),
5483
+ confirm: Z11.boolean().optional(),
5484
+ verbose: Z11.boolean().optional(),
5485
+ file: Z11.array(Z11.string()).optional(),
5486
+ apiKey: Z11.string().optional()
5487
+ }).parse(options);
5488
+ }
5489
+ async function tryAuthenticate(settings) {
5490
+ if (!settings.auth.apiKey) {
5491
+ return null;
5492
+ }
5493
+ try {
5494
+ const authenticator = createAuthenticator({
5495
+ apiKey: settings.auth.apiKey,
5496
+ apiUrl: settings.auth.apiUrl
5497
+ });
5498
+ const user = await authenticator.whoami();
5499
+ return user;
5500
+ } catch (error) {
5501
+ return null;
5502
+ }
5503
+ }
5504
+ function validateParams2(i18nConfig, flags) {
5505
+ if (!i18nConfig) {
5506
+ throw new CLIError({
5507
+ message: "i18n.json not found. Please run `lingo.dev init` to initialize the project.",
5508
+ docUrl: "i18nNotFound"
5509
+ });
5510
+ } else if (!i18nConfig.buckets || !Object.keys(i18nConfig.buckets).length) {
5511
+ throw new CLIError({
5512
+ message: "No buckets found in i18n.json. Please add at least one bucket containing i18n content.",
5513
+ docUrl: "bucketNotFound"
5514
+ });
5515
+ } else if (flags.locale?.some((locale) => !i18nConfig.locale.targets.includes(locale))) {
5516
+ throw new CLIError({
5517
+ message: `One or more specified locales do not exist in i18n.json locale.targets. Please add them to the list and try again.`,
5518
+ docUrl: "localeTargetNotFound"
5519
+ });
5520
+ } else if (flags.bucket?.some((bucket) => !i18nConfig.buckets[bucket])) {
5521
+ throw new CLIError({
5522
+ message: `One or more specified buckets do not exist in i18n.json. Please add them to the list and try again.`,
5523
+ docUrl: "bucketNotFound"
5524
+ });
5525
+ }
5526
+ }
5527
+
5114
5528
  // package.json
5115
5529
  var package_default = {
5116
5530
  name: "lingo.dev",
5117
- version: "0.84.0",
5531
+ version: "0.85.0",
5118
5532
  description: "Lingo.dev CLI",
5119
5533
  private: false,
5120
5534
  publishConfig: {
@@ -5184,6 +5598,7 @@ var package_default = {
5184
5598
  ai: "^4.3.2",
5185
5599
  bitbucket: "^2.12.0",
5186
5600
  chalk: "^5.4.1",
5601
+ "cli-table3": "^0.6.5",
5187
5602
  cors: "^2.8.5",
5188
5603
  "csv-parse": "^5.6.0",
5189
5604
  "csv-stringify": "^6.5.2",
@@ -5289,7 +5704,7 @@ ${vice(
5289
5704
 
5290
5705
  Star the the repo :) https://github.com/LingoDotDev/lingo.dev
5291
5706
  `
5292
- ).version(`v${package_default.version}`, "-v, --version", "Show version").addCommand(init_default).interactive("-y, --no-interactive", "Disable interactive mode").addCommand(i18n_default).addCommand(auth_default).addCommand(show_default).addCommand(lockfile_default).addCommand(cleanup_default).addCommand(mcp_default).addCommand(ci_default).exitOverride((err) => {
5707
+ ).version(`v${package_default.version}`, "-v, --version", "Show version").addCommand(init_default).interactive("-y, --no-interactive", "Disable interactive mode").addCommand(i18n_default).addCommand(auth_default).addCommand(show_default).addCommand(lockfile_default).addCommand(cleanup_default).addCommand(mcp_default).addCommand(ci_default).addCommand(status_default).exitOverride((err) => {
5293
5708
  if (err.code === "commander.helpDisplayed" || err.code === "commander.version" || err.code === "commander.help") {
5294
5709
  process.exit(0);
5295
5710
  }