Skip to content

Commit 6ab5faa

Browse files
author
Eric Wheeler
committed
test: add file:line to i18n key error output
Enhance i18n key test error reporting with file:line information to help locate problematic keys in the codebase. - Track line numbers for i18n keys - Handle multi-line t() calls - Show file:line in test output Signed-off-by: Eric Wheeler <[email protected]>
1 parent eb20b5a commit 6ab5faa

File tree

1 file changed

+101
-35
lines changed

1 file changed

+101
-35
lines changed

locales/__tests__/find-missing-i18n-keys.test.ts

Lines changed: 101 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ const SCAN_SOURCE_DIRS = {
1616

1717
// i18n key patterns for findMissingI18nKeys
1818
const i18nScanPatterns = [
19-
/{t\("([^"]+)"\)}/g,
19+
/\bt\([\s\n]*"([^"]+)"/g,
20+
/\bt\([\s\n]*'([^']+)'/g,
21+
/\bt\([\s\n]*`([^`]+)`/g,
2022
/i18nKey="([^"]+)"/g,
21-
/t\("([a-zA-Z][a-zA-Z0-9_]*[:.][a-zA-Z0-9_.]+)"\)/g,
22-
// Add pattern to match t() calls with parameters
23-
/t\("([a-zA-Z][a-zA-Z0-9_]*[:.][a-zA-Z0-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

299355
describe("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

Comments
 (0)