Skip to content

Conversation

@Axshatt
Copy link

@Axshatt Axshatt commented Nov 1, 2025

Adds a new CLI command to Lingo.dev that reports translation coverage across all locales — helping developers and localization teams quickly spot missing translations before release.

✨ Features

New command:
npx lingo.dev@latest show stats

Scans locale files and computes:

Total / Translated / Missing keys per locale

Coverage % with color indicators

🟢 ≥90% 🟡 ≥70% 🔴 <70%

Lists missing keys grouped by locale and file

Non-blocking (exit code 0)

🛠 Implementation

Added packages/cli/src/commands/show/stats.ts

Registered under the show command group

Added utilities in i18n.ts for file discovery + key flattening

Added tests for key counting and missing-key detection

Updated CLI help and README with examples

🔮 Next Steps

Support additional formats (YAML, PO, TS)

Add --ref-locale flag

Option to fail CI on low coverage

✅ Compatibility

Additive change , no breaking updates to existing commands or APIs.

@Axshatt Axshatt changed the title Add show stats CLI command to generate translation coverage reports Add show stats CLI command to generate translation coverage reports (#1502) Nov 1, 2025
Copilot finished reviewing on behalf of maxprilutskiy November 12, 2025 02:35
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds a new CLI command npx lingo.dev@latest show stats to generate translation coverage reports across all locales. The command scans locale files, counts total and translated keys, calculates coverage percentages with color-coded indicators, and lists missing translation keys grouped by locale and file.

Key Changes

  • Added new stats.ts command under the show command group
  • Registered the stats command in the show index
  • Added lingo.dev package dependency to root package.json

Reviewed Changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 12 comments.

File Description
packages/cli/src/cli/cmd/show/stats.ts Implements the new stats command with translation coverage calculation and reporting
packages/cli/src/cli/cmd/show/index.ts Registers the stats command with the show command group
package.json Adds lingo.dev as a dependency

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -0,0 +1,126 @@
import { Command } from "interactive-commander";
import { readConfig } from "../../utils/config";
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function readConfig does not exist in the config utility. Based on other show commands, you should use getConfig() instead, which returns I18nConfig | null and is synchronous.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,126 @@
import { Command } from "interactive-commander";
import { readConfig } from "../../utils/config";
import { getAllBucketFiles } from "../../utils/files";
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function getAllBucketFiles does not exist. Looking at similar commands like show files, you should use getBuckets(i18nConfig) from ../../utils/buckets to get bucket configurations with path patterns.

Copilot uses AI. Check for mistakes.
import { Command } from "interactive-commander";
import { readConfig } from "../../utils/config";
import { getAllBucketFiles } from "../../utils/files";
import { getLocales } from "../../utils/locales";
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function getLocales does not exist. Based on other commands, you should access target locales via i18nConfig.locale.targets after loading the config.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +55
const sourceKeys = Object.keys(JSON.parse(sourceContent));
const targetKeys = Object.keys(JSON.parse(targetContent));
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Object.keys() only counts top-level keys in JSON files and will miss nested translation keys. The codebase uses bucket loaders that flatten nested structures (e.g., {\"user\": {\"name\": \"...\", \"email\": \"...\"}} becomes user/name and user/email). You should use createBucketLoader() and call loader.pull(locale) to get properly flattened keys, similar to how the status.ts command does it (see line 231-232 in status.ts).

Copilot uses AI. Check for mistakes.
const targetKeys = Object.keys(JSON.parse(targetContent));

totalKeys += sourceKeys.length;
translatedKeys += targetKeys.length;
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This counts all keys present in the target file, not just the ones that match source keys. If a target file has extra keys not in source, or is missing some source keys, the count will be incorrect. You should count targetKeys.filter(key => sourceKeys.includes(key)).length to only count keys that exist in both source and target.

Suggested change
translatedKeys += targetKeys.length;
translatedKeys += targetKeys.filter(key => sourceKeys.includes(key)).length;

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +43
const sourcePath = file.replace("[locale]", sourceLocale);
const targetPath = file.replace("[locale]", locale);
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This simple string replacement doesn't account for locale delimiters. The codebase uses resolveOverriddenLocale(locale, bucketConfig.delimiter) to handle different delimiter conventions (e.g., en-US vs en_US). See how files.ts does this at lines 40-46 and 50-53.

Copilot uses AI. Check for mistakes.
missingKeys.push(...missing.map(key => `${path.basename(file)}: ${key}`));
}
} catch (err) {
// Skip files that can't be parsed
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Silently skipping files that can't be parsed may hide legitimate errors. Consider logging a warning or debug message when files fail to parse so users understand why certain files aren't included in stats.

Suggested change
// Skip files that can't be parsed
// Skip files that can't be parsed
console.warn(chalk.yellow(`Warning: Failed to process file "${file}" for locale "${locale}": ${err instanceof Error ? err.message : err}`));

Copilot uses AI. Check for mistakes.
const sourceLocale = config.sourceLocale || "en";
const bucketFiles = await getAllBucketFiles();

const stats: TranslationStats[] = [];
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other show commands check if config exists before proceeding (see files.ts lines 29-35 and ignored-keys.ts lines 20-26). You should add a similar check: if (!config) { throw new CLIError({ message: 'i18n.json not found. Please run lingo.dev init to initialize the project.', docUrl: 'i18nNotFound' }); }

Copilot uses AI. Check for mistakes.
"dependencies": {
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.10",
"lingo.dev": "^0.114.0",
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding lingo.dev as a dependency to the root package.json appears to be a circular dependency since this PR is modifying the lingo.dev CLI itself. This dependency is unnecessary and should be removed.

Suggested change
"lingo.dev": "^0.114.0",

Copilot uses AI. Check for mistakes.
.helpOption("-h, --help", "Show help")
.action(async () => {
try {
const config = await readConfig();
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Callee is not a function: it has type undefined.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant