Skip to content

Commit aa766db

Browse files
committed
Add CodeLens provider for localization keys
- Add CodeLens in .resx files showing reference count, language coverage, translate action, and unused/duplicate warnings - Add CodeLens in code files showing key value preview and missing language warnings - Add shared CacheService for efficient data caching across CodeLens, Resource Editor, and diagnostics - Implement client-side filtering for substring search with debounce (server-side for wildcard/regex) - Fix Resource Editor bugs: filterResources, editKey, and translate dialog issues - Add CodeLens settings (enableCodeLens, showReferences, showCoverage, showTranslate, showValue) - Change default translation provider from Lingva to MyMemory - Fix settings persistence case mismatch bug - Update README with CodeLens documentation
1 parent ee31448 commit aa766db

7 files changed

Lines changed: 950 additions & 43 deletions

File tree

vscode-extension/README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ Find missing and unused keys across your codebase.
2929

3030
![Code Scan](https://raw.githubusercontent.com/nickprotop/LocalizationManager/main/vscode-extension/images/code-scan.png)
3131

32+
### CodeLens
33+
Get inline information and actions directly in your code.
34+
35+
**In .resx files** (above each key):
36+
- Reference count (e.g., "12 references") - click to see all usages
37+
- Language coverage (e.g., "3/5 languages") - click to see missing translations
38+
- Quick translate action
39+
- Warnings for unused or duplicate keys
40+
41+
**In code files** (.cs, .razor, .xaml, .cshtml):
42+
- Key value preview (e.g., "Welcome to our app")
43+
- Missing language warnings - click to translate
44+
- Click to open the key in Resource Editor
45+
3246
### Key References
3347
See exactly where each key is used in your code.
3448

@@ -70,9 +84,14 @@ Translate missing values using free or paid providers.
7084
| Setting | Description | Default |
7185
|---------|-------------|---------|
7286
| `lrm.resourcePath` | Path to .resx folder | Auto-detected |
73-
| `lrm.translationProvider` | Default provider | `lingva` |
87+
| `lrm.translationProvider` | Default provider | `mymemory` |
7488
| `lrm.enableRealtimeScan` | Live diagnostics | `true` |
7589
| `lrm.scanOnSave` | Scan on file save | `true` |
90+
| `lrm.enableCodeLens` | Show CodeLens annotations | `true` |
91+
| `lrm.codeLens.showReferences` | Show reference count | `true` |
92+
| `lrm.codeLens.showCoverage` | Show language coverage | `true` |
93+
| `lrm.codeLens.showTranslate` | Show translate action | `true` |
94+
| `lrm.codeLens.showValue` | Show key value in code | `true` |
7695

7796
## Configuration & API Keys
7897

vscode-extension/package.json

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,26 @@
8989
{
9090
"command": "lrm.openSettings",
9191
"title": "LRM: Open Settings"
92+
},
93+
{
94+
"command": "lrm.showKeyReferences",
95+
"title": "LRM: Show Key References"
96+
},
97+
{
98+
"command": "lrm.showMissingLanguages",
99+
"title": "LRM: Show Missing Languages"
100+
},
101+
{
102+
"command": "lrm.translateKeyFromLens",
103+
"title": "LRM: Translate Key"
104+
},
105+
{
106+
"command": "lrm.editKeyFromLens",
107+
"title": "LRM: Edit Key"
108+
},
109+
{
110+
"command": "lrm.deleteUnusedKey",
111+
"title": "LRM: Delete Unused Key"
92112
}
93113
],
94114
"configuration": {
@@ -121,7 +141,7 @@
121141
"lingva",
122142
"mymemory"
123143
],
124-
"default": "lingva",
144+
"default": "mymemory",
125145
"description": "Default translation provider"
126146
},
127147
"lrm.autoOpenBrowser": {
@@ -164,6 +184,31 @@
164184
"type": "boolean",
165185
"default": true,
166186
"description": "Enable validation of placeholders in resource strings"
187+
},
188+
"lrm.enableCodeLens": {
189+
"type": "boolean",
190+
"default": true,
191+
"description": "Show CodeLens for localization keys in .resx and code files"
192+
},
193+
"lrm.codeLens.showReferences": {
194+
"type": "boolean",
195+
"default": true,
196+
"description": "Show reference count in CodeLens (e.g., '12 references')"
197+
},
198+
"lrm.codeLens.showCoverage": {
199+
"type": "boolean",
200+
"default": true,
201+
"description": "Show language coverage in CodeLens (e.g., '3/5 languages')"
202+
},
203+
"lrm.codeLens.showTranslate": {
204+
"type": "boolean",
205+
"default": true,
206+
"description": "Show 'Translate' action in CodeLens"
207+
},
208+
"lrm.codeLens.showValue": {
209+
"type": "boolean",
210+
"default": true,
211+
"description": "Show key value in CodeLens for code files"
167212
}
168213
}
169214
},
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { ApiClient, ScanResult, ResourceKeyDetails, ValidationResult, KeyUsage, ResourceKey } from './apiClient';
2+
3+
/**
4+
* Shared cache service for the LRM extension.
5+
* Provides centralized caching for scan results, key details, and validation data.
6+
* Used by CodeLens provider, Resource Editor, and diagnostics.
7+
*/
8+
export class CacheService {
9+
private scanResultsCache: ScanResult | null = null;
10+
private keyDetailsCache: Map<string, ResourceKeyDetails> = new Map();
11+
private keysCache: ResourceKey[] | null = null;
12+
private validationCache: ValidationResult | null = null;
13+
private keyReferencesCache: Map<string, KeyUsage> = new Map();
14+
15+
private scanResultsTimestamp: number = 0;
16+
private validationTimestamp: number = 0;
17+
private keysTimestamp: number = 0;
18+
19+
private readonly TTL = 30000; // 30 seconds cache TTL
20+
21+
constructor(private apiClient: ApiClient) {}
22+
23+
/**
24+
* Check if a timestamp has expired based on TTL
25+
*/
26+
private isExpired(timestamp: number): boolean {
27+
return Date.now() - timestamp > this.TTL;
28+
}
29+
30+
/**
31+
* Get scan results (cached or fresh)
32+
*/
33+
async getScanResults(forceRefresh = false): Promise<ScanResult> {
34+
if (!forceRefresh && this.scanResultsCache && !this.isExpired(this.scanResultsTimestamp)) {
35+
return this.scanResultsCache;
36+
}
37+
38+
this.scanResultsCache = await this.apiClient.scanCode();
39+
this.scanResultsTimestamp = Date.now();
40+
41+
// Also populate key references cache from scan results
42+
this.keyReferencesCache.clear();
43+
for (const ref of this.scanResultsCache.references) {
44+
this.keyReferencesCache.set(ref.key, ref);
45+
}
46+
47+
return this.scanResultsCache;
48+
}
49+
50+
/**
51+
* Get all resource keys (cached or fresh)
52+
*/
53+
async getKeys(forceRefresh = false): Promise<ResourceKey[]> {
54+
if (!forceRefresh && this.keysCache && !this.isExpired(this.keysTimestamp)) {
55+
return this.keysCache;
56+
}
57+
58+
this.keysCache = await this.apiClient.getKeys();
59+
this.keysTimestamp = Date.now();
60+
return this.keysCache;
61+
}
62+
63+
/**
64+
* Get details for a specific key (cached or fresh)
65+
*/
66+
async getKeyDetails(key: string, forceRefresh = false): Promise<ResourceKeyDetails> {
67+
if (!forceRefresh && this.keyDetailsCache.has(key)) {
68+
return this.keyDetailsCache.get(key)!;
69+
}
70+
71+
const details = await this.apiClient.getKeyDetails(key);
72+
this.keyDetailsCache.set(key, details);
73+
return details;
74+
}
75+
76+
/**
77+
* Get validation results (cached or fresh)
78+
*/
79+
async getValidation(forceRefresh = false): Promise<ValidationResult> {
80+
if (!forceRefresh && this.validationCache && !this.isExpired(this.validationTimestamp)) {
81+
return this.validationCache;
82+
}
83+
84+
this.validationCache = await this.apiClient.validate();
85+
this.validationTimestamp = Date.now();
86+
return this.validationCache;
87+
}
88+
89+
/**
90+
* Get references for a specific key (from cached scan results or API)
91+
*/
92+
async getKeyReferences(key: string, forceRefresh = false): Promise<KeyUsage> {
93+
// Try to get from cache first
94+
if (!forceRefresh && this.keyReferencesCache.has(key)) {
95+
return this.keyReferencesCache.get(key)!;
96+
}
97+
98+
// If we have scan results, the key might just not have any references
99+
if (!forceRefresh && this.scanResultsCache && !this.isExpired(this.scanResultsTimestamp)) {
100+
const cached = this.keyReferencesCache.get(key);
101+
if (cached) {
102+
return cached;
103+
}
104+
// Key not in references means 0 references
105+
return { key, referenceCount: 0, references: [] };
106+
}
107+
108+
// Fetch fresh from API
109+
const usage = await this.apiClient.getKeyReferences(key);
110+
this.keyReferencesCache.set(key, usage);
111+
return usage;
112+
}
113+
114+
/**
115+
* Get reference count for a key (quick lookup from cache)
116+
*/
117+
getReferenceCountFromCache(key: string): number | null {
118+
const cached = this.keyReferencesCache.get(key);
119+
return cached ? cached.referenceCount : null;
120+
}
121+
122+
/**
123+
* Check if a key is in the unused list (from cached scan results)
124+
*/
125+
isKeyUnused(key: string): boolean | null {
126+
if (!this.scanResultsCache || !Array.isArray(this.scanResultsCache.unused)) {
127+
return null;
128+
}
129+
return this.scanResultsCache.unused.includes(key);
130+
}
131+
132+
/**
133+
* Check if a key has duplicates (from cached validation)
134+
*/
135+
isKeyDuplicate(key: string): boolean | null {
136+
if (!this.validationCache || !Array.isArray(this.validationCache.duplicateKeys)) {
137+
return null;
138+
}
139+
return this.validationCache.duplicateKeys.includes(key);
140+
}
141+
142+
/**
143+
* Get missing languages for a key (from cached key details)
144+
*/
145+
getMissingLanguages(key: string): string[] | null {
146+
const details = this.keyDetailsCache.get(key);
147+
if (!details) {
148+
return null;
149+
}
150+
151+
// Find languages with empty values
152+
const missing: string[] = [];
153+
for (const [lang, data] of Object.entries(details.values)) {
154+
if (!data.value || data.value.trim() === '') {
155+
missing.push(lang);
156+
}
157+
}
158+
return missing;
159+
}
160+
161+
/**
162+
* Invalidate all caches (call when .resx files change)
163+
*/
164+
invalidate(): void {
165+
this.scanResultsCache = null;
166+
this.keyDetailsCache.clear();
167+
this.keysCache = null;
168+
this.validationCache = null;
169+
this.keyReferencesCache.clear();
170+
this.scanResultsTimestamp = 0;
171+
this.validationTimestamp = 0;
172+
this.keysTimestamp = 0;
173+
}
174+
175+
/**
176+
* Invalidate only key-specific caches (for when a single key is modified)
177+
*/
178+
invalidateKey(key: string): void {
179+
this.keyDetailsCache.delete(key);
180+
this.keyReferencesCache.delete(key);
181+
// Also invalidate keys list and validation since they might be affected
182+
this.keysCache = null;
183+
this.validationCache = null;
184+
this.keysTimestamp = 0;
185+
this.validationTimestamp = 0;
186+
}
187+
188+
/**
189+
* Check if cache has any data (for UI status)
190+
*/
191+
hasData(): boolean {
192+
return this.scanResultsCache !== null || this.keysCache !== null;
193+
}
194+
195+
/**
196+
* Get cached scan results if available (does NOT fetch from API)
197+
* Returns null if no cached results or if cache is expired
198+
*/
199+
getCachedScanResults(): ScanResult | null {
200+
if (this.scanResultsCache && !this.isExpired(this.scanResultsTimestamp)) {
201+
return this.scanResultsCache;
202+
}
203+
return null;
204+
}
205+
}

0 commit comments

Comments
 (0)