@@ -23,7 +23,7 @@ const i18nScanPatterns = [
2323]
2424
2525// Track line numbers for source code keys
26- const lineMap = new Map < string , { file : string , line : number } > ( )
26+ const lineMap = new Map < string , { file : string ; line : number } > ( )
2727
2828// Check if the key exists in all official language files, return a list of missing language files
2929
@@ -53,12 +53,12 @@ function accumulateSourceKeys(): Set<string> {
5353 let match
5454 while ( ( match = pattern . exec ( content ) ) !== null ) {
5555 const key = match [ 1 ]
56- const lineNumber = content . slice ( 0 , match . index ) . split ( '\n' ) . length
56+ const lineNumber = content . slice ( 0 , match . index ) . split ( "\n" ) . length
5757 matches . add ( key )
5858 sourceCodeKeys . add ( key )
5959 lineMap . set ( key , {
6060 file : path . relative ( process . cwd ( ) , filePath ) ,
61- line : lineNumber
61+ line : lineNumber ,
6262 } )
6363 }
6464 }
@@ -139,24 +139,57 @@ function getKeysInSourceNotInTranslation(sourceKeys: Set<string>, translationKey
139139 . sort ( )
140140}
141141
142+ // Function to convert a key into segments and mark dynamic parts as undefined
143+ function keyToSegments ( key : string ) : ( string | undefined ) [ ] {
144+ return key . split ( "." ) . map ( ( segment ) => ( segment . includes ( "${" ) ? undefined : segment ) )
145+ }
146+
147+ // Function to check if a static key matches a dynamic key pattern
148+ function matchesKeyPattern ( staticKey : string , dynamicKey : string ) : boolean {
149+ const staticSegments = staticKey . split ( "." )
150+ const dynamicSegments = keyToSegments ( dynamicKey )
151+
152+ if ( staticSegments . length !== dynamicSegments . length ) {
153+ return false
154+ }
155+
156+ return dynamicSegments . every ( ( dynSeg , i ) => dynSeg === undefined || dynSeg === staticSegments [ i ] )
157+ }
158+
142159// Function 4: Return all keys in translations that are not in source
143- function getKeysInTranslationNotInSource ( sourceKeys : Set < string > , translationKeys : Set < string > ) : string [ ] {
160+ function getKeysInTranslationNotInSource (
161+ sourceKeys : Set < string > ,
162+ translationKeys : Set < string > ,
163+ dynamicKeys : string [ ] = [ ] ,
164+ ) : string [ ] {
144165 return Array . from ( translationKeys )
145- . filter ( ( key ) => ! sourceKeys . has ( key ) )
166+ . filter ( ( key ) => {
167+ // If key is directly used in source, it's not unused
168+ if ( sourceKeys . has ( key ) ) {
169+ return false
170+ }
171+
172+ // If key matches any dynamic key pattern, it's not unused
173+ if ( dynamicKeys . some ( ( dynamicKey ) => matchesKeyPattern ( key , dynamicKey ) ) ) {
174+ return false
175+ }
176+
177+ return true
178+ } )
146179 . sort ( )
147180}
148181
149182// Function to find dynamic i18n keys (containing ${...})
150183function findDynamicKeys ( sourceKeys : Set < string > ) : string [ ] {
151184 return Array . from ( sourceKeys )
152- . filter ( key => key . includes ( "${" ) )
185+ . filter ( ( key ) => key . includes ( "${" ) )
153186 . sort ( )
154187}
155188
156189// Function to find non-namespaced t() calls
157190export function findNonNamespacedI18nKeys ( sourceKeys : Set < string > ) : string [ ] {
158191 return Array . from ( sourceKeys )
159- . filter ( key => ! key . includes ( ":" ) )
192+ . filter ( ( key ) => ! key . includes ( ":" ) )
160193 . sort ( )
161194}
162195
@@ -187,20 +220,21 @@ export function findMissingI18nKeys(): { output: string; nonNamespacedKeys: stri
187220 // Get source code keys and translation keys
188221 const sourceCodeKeys = accumulateSourceKeys ( )
189222 const translationFileKeys = accumulateTranslationKeys ( )
190-
223+
191224 // Find special keys
192225 const dynamicKeys = findDynamicKeys ( sourceCodeKeys )
193226 const nonNamespacedKeys = findNonNamespacedI18nKeys ( sourceCodeKeys )
194227
195228 // Create sets for set operations
196229 const dynamicSet = new Set ( dynamicKeys )
197230 const nonNamespacedSet = new Set ( nonNamespacedKeys )
198- const remainingSourceKeys = new Set ( Array . from ( sourceCodeKeys )
199- . filter ( key => ! dynamicSet . has ( key ) && ! nonNamespacedSet . has ( key ) ) )
231+ const remainingSourceKeys = new Set (
232+ Array . from ( sourceCodeKeys ) . filter ( ( key ) => ! dynamicSet . has ( key ) && ! nonNamespacedSet . has ( key ) ) ,
233+ )
200234
201235 // Find keys in source not in translations and vice versa
202236 const missingTranslationKeys = getKeysInSourceNotInTranslation ( remainingSourceKeys , translationFileKeys )
203- const unusedTranslationKeys = getKeysInTranslationNotInSource ( remainingSourceKeys , translationFileKeys )
237+ const unusedTranslationKeys = getKeysInTranslationNotInSource ( remainingSourceKeys , translationFileKeys , dynamicKeys )
204238
205239 // Track unused keys in English locale files
206240 const unusedKeys : Array < { key : string ; file : string } > = [ ]
@@ -242,13 +276,13 @@ export function findMissingI18nKeys(): { output: string; nonNamespacedKeys: stri
242276 // Add summary counts
243277 summaryOutput += `\nTotal source code keys: ${ sourceCodeKeys . size } \n`
244278 summaryOutput += `Total translation file keys: ${ translationFileKeys . size } \n`
245-
279+
246280 // Dynamic keys
247281 summaryOutput += `\n1. Dynamic i18n keys (${ dynamicKeys . length } ):\n`
248282 if ( dynamicKeys . length === 0 ) {
249283 summaryOutput += " None - all i18n keys are static\n"
250284 } else {
251- dynamicKeys . forEach ( key => {
285+ dynamicKeys . forEach ( ( key ) => {
252286 const loc = lineMap . get ( key )
253287 summaryOutput += ` - ${ loc ?. file } :${ loc ?. line } : ${ key } \n`
254288 } )
@@ -259,7 +293,7 @@ export function findMissingI18nKeys(): { output: string; nonNamespacedKeys: stri
259293 if ( nonNamespacedKeys . length === 0 ) {
260294 summaryOutput += " None - all t() calls use namespaces\n"
261295 } else {
262- nonNamespacedKeys . forEach ( key => {
296+ nonNamespacedKeys . forEach ( ( key ) => {
263297 const loc = lineMap . get ( key )
264298 summaryOutput += ` - ${ loc ?. file } :${ loc ?. line } : ${ key } \n`
265299 } )
@@ -275,10 +309,10 @@ export function findMissingI18nKeys(): { output: string; nonNamespacedKeys: stri
275309 } )
276310 }
277311
278- // Keys in translation files but not in source code
279- summaryOutput += `\n4. Keys in translation files but not in source code (${ unusedTranslationKeys . length } ):\n`
312+ // Keys in translation files but not in source code (excluding dynamic matches)
313+ summaryOutput += `\n4. Unused translation keys (${ unusedTranslationKeys . length } ):\n`
280314 if ( unusedTranslationKeys . length === 0 ) {
281- summaryOutput += " None - all translation keys are used in source code \n"
315+ summaryOutput += " None - all translation keys are either directly used or matched by dynamic patterns \n"
282316 } else {
283317 // Group keys by locale directory and file
284318 const localeFileMap = new Map < string , Map < string , string [ ] > > ( )
@@ -348,7 +382,7 @@ export function findMissingI18nKeys(): { output: string; nonNamespacedKeys: stri
348382
349383 return {
350384 output : printLogs ( ) ,
351- nonNamespacedKeys
385+ nonNamespacedKeys,
352386 }
353387}
354388
@@ -371,12 +405,17 @@ describe("Find Missing i18n Keys", () => {
371405 // Create sets for set operations
372406 const dynamicSet = new Set ( dynamicKeys )
373407 const nonNamespacedSet = new Set ( nonNamespacedKeys )
374- const remainingSourceKeys = new Set ( Array . from ( sourceKeys )
375- . filter ( key => ! dynamicSet . has ( key ) && ! nonNamespacedSet . has ( key ) ) )
408+ const remainingSourceKeys = new Set (
409+ Array . from ( sourceKeys ) . filter ( ( key ) => ! dynamicSet . has ( key ) && ! nonNamespacedSet . has ( key ) ) ,
410+ )
376411
377412 // Find differences
378413 keysInSourceNotInTranslation = getKeysInSourceNotInTranslation ( remainingSourceKeys , translationKeys )
379- keysInTranslationNotInSource = getKeysInTranslationNotInSource ( remainingSourceKeys , translationKeys )
414+ keysInTranslationNotInSource = getKeysInTranslationNotInSource (
415+ remainingSourceKeys ,
416+ translationKeys ,
417+ dynamicKeys ,
418+ )
380419
381420 // Clear logs at start
382421 clearLogs ( )
0 commit comments