@@ -16,13 +16,15 @@ const SCAN_SOURCE_DIRS = {
1616
1717// i18n key patterns for findMissingI18nKeys
1818const i18nScanPatterns = [
19- / { t \( " ( [ ^ " ] + ) " \) } / g,
19+ / \b t \( [ \s \n ] * " ( [ ^ " ] + ) " / g,
20+ / \b t \( [ \s \n ] * ' ( [ ^ ' ] + ) ' / g,
21+ / \b t \( [ \s \n ] * ` ( [ ^ ` ] + ) ` / g,
2022 / i 1 8 n K e y = " ( [ ^ " ] + ) " / g,
21- / t \( " ( [ a - z A - Z ] [ a - z A - Z 0 - 9 _ ] * [: .] [ a - z A - Z 0 - 9 _ . ] + ) " \) / g,
22- // Add pattern to match t() calls with parameters
23- / t \( " ( [ a - z A - Z ] [ a - z A - Z 0 - 9 _ ] * [: .] [ a - z A - Z 0 - 9 _ . ] + ) " , / g,
2423]
2524
25+ // Track line numbers for source code keys
26+ const lineMap = new Map < string , { file : string , line : number } > ( )
27+
2628// Check if the key exists in all official language files, return a list of missing language files
2729
2830// Function 1: Accumulate source keys
@@ -50,8 +52,14 @@ function accumulateSourceKeys(): Set<string> {
5052 for ( const pattern of i18nScanPatterns ) {
5153 let match
5254 while ( ( match = pattern . exec ( content ) ) !== null ) {
53- matches . add ( match [ 1 ] )
54- sourceCodeKeys . add ( match [ 1 ] ) // Add to global set for unused key detection
55+ const key = match [ 1 ]
56+ const lineNumber = content . slice ( 0 , match . index ) . split ( '\n' ) . length
57+ matches . add ( key )
58+ sourceCodeKeys . add ( key )
59+ lineMap . set ( key , {
60+ file : path . relative ( process . cwd ( ) , filePath ) ,
61+ line : lineNumber
62+ } )
5563 }
5664 }
5765 }
@@ -138,17 +146,61 @@ function getKeysInTranslationNotInSource(sourceKeys: Set<string>, translationKey
138146 . sort ( )
139147}
140148
141- // Recursively traverse the directory
142- export function findMissingI18nKeys ( ) : { output : string } {
149+ // Function to find dynamic i18n keys (containing ${...})
150+ function findDynamicKeys ( sourceKeys : Set < string > ) : string [ ] {
151+ return Array . from ( sourceKeys )
152+ . filter ( key => key . includes ( "${" ) )
153+ . sort ( )
154+ }
155+
156+ // Function to find non-namespaced t() calls
157+ export function findNonNamespacedI18nKeys ( sourceKeys : Set < string > ) : string [ ] {
158+ return Array . from ( sourceKeys )
159+ . filter ( key => ! key . includes ( ":" ) )
160+ . sort ( )
161+ }
162+
163+ export function findMissingI18nKeys ( ) : { output : string ; nonNamespacedKeys : string [ ] } {
143164 clearLogs ( ) // Clear buffer at start
144165
166+ // Helper function to extract all keys from a JSON object with their full paths
167+ function extractKeysFromJson ( obj : any , prefix : string ) : string [ ] {
168+ const keys : string [ ] = [ ]
169+
170+ function traverse ( o : any , p : string ) {
171+ if ( o && typeof o === "object" ) {
172+ Object . keys ( o ) . forEach ( ( key ) => {
173+ const newPath = p ? `${ p } .${ key } ` : key
174+ if ( o [ key ] && typeof o [ key ] === "object" ) {
175+ traverse ( o [ key ] , newPath )
176+ } else {
177+ keys . push ( `${ prefix } :${ newPath } ` )
178+ }
179+ } )
180+ }
181+ }
182+
183+ traverse ( obj , "" )
184+ return keys
185+ }
186+
145187 // Get source code keys and translation keys
146188 const sourceCodeKeys = accumulateSourceKeys ( )
147189 const translationFileKeys = accumulateTranslationKeys ( )
190+
191+ // Find special keys
192+ const dynamicKeys = findDynamicKeys ( sourceCodeKeys )
193+ const nonNamespacedKeys = findNonNamespacedI18nKeys ( sourceCodeKeys )
194+
195+ // Create sets for set operations
196+ const dynamicSet = new Set ( dynamicKeys )
197+ const nonNamespacedSet = new Set ( nonNamespacedKeys )
198+ const remainingSourceKeys = new Set ( Array . from ( sourceCodeKeys )
199+ . filter ( key => ! dynamicSet . has ( key ) && ! nonNamespacedSet . has ( key ) ) )
148200
149201 // Find keys in source not in translations and vice versa
150- const missingTranslationKeys = getKeysInSourceNotInTranslation ( sourceCodeKeys , translationFileKeys )
151- const unusedTranslationKeys = getKeysInTranslationNotInSource ( sourceCodeKeys , translationFileKeys )
202+ const missingTranslationKeys = getKeysInSourceNotInTranslation ( remainingSourceKeys , translationFileKeys )
203+ const unusedTranslationKeys = getKeysInTranslationNotInSource ( remainingSourceKeys , translationFileKeys )
152204
153205 // Track unused keys in English locale files
154206 const unusedKeys : Array < { key : string ; file : string } > = [ ]
@@ -190,9 +242,31 @@ export function findMissingI18nKeys(): { output: string } {
190242 // Add summary counts
191243 summaryOutput += `\nTotal source code keys: ${ sourceCodeKeys . size } \n`
192244 summaryOutput += `Total translation file keys: ${ translationFileKeys . size } \n`
245+
246+ // Dynamic keys
247+ summaryOutput += `\n1. Dynamic i18n keys (${ dynamicKeys . length } ):\n`
248+ if ( dynamicKeys . length === 0 ) {
249+ summaryOutput += " None - all i18n keys are static\n"
250+ } else {
251+ dynamicKeys . forEach ( key => {
252+ const loc = lineMap . get ( key )
253+ summaryOutput += ` - ${ loc ?. file } :${ loc ?. line } : ${ key } \n`
254+ } )
255+ }
256+
257+ // Non-namespaced keys
258+ summaryOutput += `\n2. Non-namespaced t() calls (${ nonNamespacedKeys . length } ):\n`
259+ if ( nonNamespacedKeys . length === 0 ) {
260+ summaryOutput += " None - all t() calls use namespaces\n"
261+ } else {
262+ nonNamespacedKeys . forEach ( key => {
263+ const loc = lineMap . get ( key )
264+ summaryOutput += ` - ${ loc ?. file } :${ loc ?. line } : ${ key } \n`
265+ } )
266+ }
193267
194268 // Keys in source code but not in translation files
195- summaryOutput += `\n1 . Keys in source code but not in translation files (${ missingTranslationKeys . length } ):\n`
269+ summaryOutput += `\n3 . Keys in source code but not in translation files (${ missingTranslationKeys . length } ):\n`
196270 if ( missingTranslationKeys . length === 0 ) {
197271 summaryOutput += " None - all source code keys have translations\n"
198272 } else {
@@ -202,7 +276,7 @@ export function findMissingI18nKeys(): { output: string } {
202276 }
203277
204278 // Keys in translation files but not in source code
205- summaryOutput += `\n2 . Keys in translation files but not in source code (${ unusedTranslationKeys . length } ):\n`
279+ summaryOutput += `\n4 . Keys in translation files but not in source code (${ unusedTranslationKeys . length } ):\n`
206280 if ( unusedTranslationKeys . length === 0 ) {
207281 summaryOutput += " None - all translation keys are used in source code\n"
208282 } else {
@@ -272,28 +346,10 @@ export function findMissingI18nKeys(): { output: string } {
272346 // Add to buffer as a single log entry
273347 bufferLog ( summaryOutput )
274348
275- // Helper function to extract all keys from a JSON object with their full paths
276- function extractKeysFromJson ( obj : any , prefix : string ) : string [ ] {
277- const keys : string [ ] = [ ]
278-
279- function traverse ( o : any , p : string ) {
280- if ( o && typeof o === "object" ) {
281- Object . keys ( o ) . forEach ( ( key ) => {
282- const newPath = p ? `${ p } .${ key } ` : key
283- if ( o [ key ] && typeof o [ key ] === "object" ) {
284- traverse ( o [ key ] , newPath )
285- } else {
286- keys . push ( `${ prefix } :${ newPath } ` )
287- }
288- } )
289- }
290- }
291-
292- traverse ( obj , "" )
293- return keys
349+ return {
350+ output : printLogs ( ) ,
351+ nonNamespacedKeys
294352 }
295-
296- return { output : printLogs ( ) }
297353}
298354
299355describe ( "Find Missing i18n Keys" , ( ) => {
@@ -308,9 +364,19 @@ describe("Find Missing i18n Keys", () => {
308364 sourceKeys = accumulateSourceKeys ( )
309365 translationKeys = accumulateTranslationKeys ( )
310366
367+ // Find special keys
368+ const dynamicKeys = findDynamicKeys ( sourceKeys )
369+ const nonNamespacedKeys = findNonNamespacedI18nKeys ( sourceKeys )
370+
371+ // Create sets for set operations
372+ const dynamicSet = new Set ( dynamicKeys )
373+ const nonNamespacedSet = new Set ( nonNamespacedKeys )
374+ const remainingSourceKeys = new Set ( Array . from ( sourceKeys )
375+ . filter ( key => ! dynamicSet . has ( key ) && ! nonNamespacedSet . has ( key ) ) )
376+
311377 // Find differences
312- keysInSourceNotInTranslation = getKeysInSourceNotInTranslation ( sourceKeys , translationKeys )
313- keysInTranslationNotInSource = getKeysInTranslationNotInSource ( sourceKeys , translationKeys )
378+ keysInSourceNotInTranslation = getKeysInSourceNotInTranslation ( remainingSourceKeys , translationKeys )
379+ keysInTranslationNotInSource = getKeysInTranslationNotInSource ( remainingSourceKeys , translationKeys )
314380
315381 // Clear logs at start
316382 clearLogs ( )
0 commit comments