@@ -367,25 +367,102 @@ function formatResults(results: Results, checkTypes: string[], options: LintOpti
367367 } )
368368
369369 // Group by locale first
370- const byLocale = new Map < string , string [ ] > ( )
370+ const missingFilesByLocale = new Map < string , string [ ] > ( )
371+ const missingKeysByLocale = new Map < string , Map < string , Set < string > > > ( )
372+
371373 missingByFile . forEach ( ( keys , fileAndLang ) => {
372374 const [ file , lang ] = fileAndLang . split ( ":" )
373- if ( ! byLocale . has ( lang ) ) {
374- byLocale . set ( lang , [ ] )
375- }
376375 const mapping = mappings . find ( ( m ) => m . area === area )
377- if ( mapping ) {
378- const targetPath = resolveTargetPath ( file , mapping . targetTemplate , lang )
379- byLocale . get ( lang ) ?. push ( targetPath )
376+ if ( ! mapping ) return
377+
378+ const targetPath = resolveTargetPath ( file , mapping . targetTemplate , lang )
379+
380+ // Check if this is a missing file or missing keys
381+ let isMissingFile = false
382+
383+ // Check for the special "File missing" case
384+ if ( keys . size === 1 ) {
385+ const key = Array . from ( keys ) [ 0 ]
386+ // Either the key equals the source file or it's a file path
387+ isMissingFile = key === file || key . includes ( "/" )
388+ }
389+
390+ if ( isMissingFile ) {
391+ // This is a missing file
392+ if ( ! missingFilesByLocale . has ( lang ) ) {
393+ missingFilesByLocale . set ( lang , [ ] )
394+ }
395+ missingFilesByLocale . get ( lang ) ?. push ( targetPath )
396+ } else {
397+ // These are missing keys
398+ if ( ! missingKeysByLocale . has ( lang ) ) {
399+ missingKeysByLocale . set ( lang , new Map ( ) )
400+ }
401+ if ( ! missingKeysByLocale . get ( lang ) ?. has ( targetPath ) ) {
402+ missingKeysByLocale . get ( lang ) ?. set ( targetPath , new Set ( ) )
403+ }
404+ keys . forEach ( ( key ) => {
405+ // Skip keys that look like file paths
406+ if ( ! key . includes ( "/" ) ) {
407+ missingKeysByLocale . get ( lang ) ?. get ( targetPath ) ?. add ( key )
408+ }
409+ } )
380410 }
381411 } )
382412
383- byLocale . forEach ( ( files , lang ) => {
413+ // Report missing files
414+ missingFilesByLocale . forEach ( ( files , lang ) => {
384415 bufferLog ( ` ${ lang } : missing ${ files . length } files` )
385416 files . sort ( ) . forEach ( ( file ) => {
386417 bufferLog ( ` ${ file } ` )
418+
419+ // Show missing keys for missing files too
420+ const sourceFile = file . replace ( `/${ lang } /` , "/en/" )
421+ const sourceContent = parseJsonContent ( loadFileContent ( sourceFile ) , sourceFile )
422+
423+ if ( sourceContent ) {
424+ bufferLog ( ` Missing keys:` )
425+ const sourceKeys = findKeys ( sourceContent )
426+ sourceKeys . sort ( ) . forEach ( ( key ) => {
427+ const englishValue = getValueAtPath ( sourceContent , key )
428+ if ( typeof englishValue === "string" ) {
429+ bufferLog ( ` - ${ key } - '${ englishValue } ' [en]` )
430+ }
431+ } )
432+ }
387433 } )
388434 } )
435+
436+ // Report files with missing keys
437+ missingKeysByLocale . forEach ( ( fileMap , lang ) => {
438+ const filesWithMissingKeys = Array . from ( fileMap . keys ( ) )
439+ if ( filesWithMissingKeys . length > 0 ) {
440+ bufferLog ( ` ${ lang } : ${ filesWithMissingKeys . length } files with missing translations` )
441+ filesWithMissingKeys . sort ( ) . forEach ( ( file ) => {
442+ bufferLog ( ` ${ file } ` )
443+ const keys = fileMap . get ( file )
444+ if ( keys && keys . size > 0 ) {
445+ bufferLog ( ` Missing keys:` )
446+ // Get the source file to extract English values
447+ const sourceFile = file . replace ( `/${ lang } /` , "/en/" )
448+ const sourceContent = parseJsonContent ( loadFileContent ( sourceFile ) , sourceFile )
449+
450+ Array . from ( keys )
451+ . sort ( )
452+ . forEach ( ( key ) => {
453+ const englishValue = sourceContent
454+ ? getValueAtPath ( sourceContent , key )
455+ : undefined
456+
457+ // Skip displaying complex objects
458+ if ( typeof englishValue === "string" ) {
459+ bufferLog ( ` - ${ key } - '${ englishValue } ' [en]` )
460+ }
461+ } )
462+ }
463+ } )
464+ }
465+ } )
389466 }
390467
391468 // Show extra translations if any
@@ -519,7 +596,7 @@ function printUsage(): void {
519596 bufferLog ( " Example: --file=settings.json,commands.json" )
520597 bufferLog ( " Use 'all' for all files" )
521598 bufferLog ( " --area=<areas> Filter by specific areas (comma-separated)" )
522- bufferLog ( ` Valid areas: ${ PATH_MAPPINGS . map ( m => m . area ) . join ( ", " ) } , all` )
599+ bufferLog ( ` Valid areas: ${ PATH_MAPPINGS . map ( ( m ) => m . area ) . join ( ", " ) } , all` )
523600 bufferLog ( " Example: --area=docs,core" )
524601 bufferLog ( " --check=<checks> Specify which checks to run (comma-separated)" )
525602 bufferLog ( " Valid checks: missing, extra, all" )
@@ -567,7 +644,7 @@ function parseArgs(args: string[] = process.argv.slice(2)): LintOptions {
567644 options . locale = values
568645 // Override the global languages array with the provided locales
569646 // Add 'en' as it's always needed as the source language
570- languages = [ 'en' , ...values ] as unknown as Language [ ]
647+ languages = [ "en" , ...values ] as unknown as Language [ ]
571648 break
572649 case "file" :
573650 options . file = values
@@ -602,13 +679,13 @@ function parseArgs(args: string[] = process.argv.slice(2)): LintOptions {
602679function lintTranslations ( args ?: LintOptions ) : { output : string } {
603680 clearLogs ( ) // Clear the buffer at the start
604681 const options = args || parseArgs ( ) || { area : [ "all" ] , check : [ "all" ] }
605-
682+
606683 // If help flag is set, print usage and return
607684 if ( options . help ) {
608685 printUsage ( )
609686 return { output : printLogs ( ) }
610687 }
611-
688+
612689 const checksToRun = options . check ?. includes ( "all" ) ? [ "missing" , "extra" ] : options . check || [ "all" ]
613690
614691 const filteredMappings = filterMappingsByArea ( PATH_MAPPINGS , options . area )
@@ -680,22 +757,22 @@ describe("Translation Linting", () => {
680757 test ( "Run translation linting" , ( ) => {
681758 // Use the centralized parseArgs function to process Jest arguments
682759 // Jest passes arguments after -- to the test
683- const options = parseArgs ( process . argv ) ;
684-
760+ const options = parseArgs ( process . argv )
761+
685762 // If help flag is set, run with help option
686763 if ( options . help ) {
687- const result = lintTranslations ( options ) ;
688- console . log ( result . output ) ; // Print help directly to console for visibility
689- expect ( result . output ) . toContain ( "Usage: node lint-translations.js [options]" ) ;
690- return ;
764+ const result = lintTranslations ( options )
765+ console . log ( result . output ) // Print help directly to console for visibility
766+ expect ( result . output ) . toContain ( "Usage: node lint-translations.js [options]" )
767+ return
691768 }
692-
769+
693770 // Run with processed options
694- const result = lintTranslations ( options ) ;
695-
771+ const result = lintTranslations ( options )
772+
696773 // MUST FAIL in ANY event where the output does not contain "All translations are complete"
697774 // This will cause the test to fail for locales with missing or extra translations
698- expect ( result . output ) . toContain ( "All translations are complete" ) ;
775+ expect ( result . output ) . toContain ( "All translations are complete" )
699776 } )
700777
701778 test ( "Filters mappings by area correctly" , ( ) => {
@@ -708,15 +785,15 @@ describe("Translation Linting", () => {
708785 const result = lintTranslations ( {
709786 help : true ,
710787 area : [ "all" ] ,
711- check : [ "all" ]
788+ check : [ "all" ] ,
712789 } )
713-
790+
714791 // Verify help content
715792 expect ( result . output ) . toContain ( "Usage: node lint-translations.js [options]" )
716793 expect ( result . output ) . toContain ( "Description:" )
717794 expect ( result . output ) . toContain ( "Options:" )
718795 expect ( result . output ) . toContain ( "Examples:" )
719-
796+
720797 // Verify it doesn't run the linting process
721798 expect ( result . output ) . not . toContain ( "Translation Results" )
722799 } )
0 commit comments