Skip to content

Commit 952303b

Browse files
committed
Add Android and iOS support to VS Code extension
- Update ResourceFormat type to include android/ios - Add file detection for Android (res/values*/strings.xml) - Add file detection for iOS (*.lproj/*.strings) - Add file watchers for Android and iOS resource files - Update format picker to show Android/iOS options
1 parent 8eaba5c commit 952303b

3 files changed

Lines changed: 95 additions & 16 deletions

File tree

vscode-extension/src/backend/cacheService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ApiClient, ScanResult, ResourceKeyDetails, ValidationResult, KeyUsage,
33
/**
44
* Resource format type
55
*/
6-
export type ResourceFormat = 'resx' | 'json' | null;
6+
export type ResourceFormat = 'resx' | 'json' | 'i18next' | 'android' | 'ios' | null;
77

88
/**
99
* Shared cache service for the LRM extension.

vscode-extension/src/backend/lrmService.ts

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -460,12 +460,14 @@ export class LrmService implements vscode.Disposable {
460460
return null;
461461
}
462462

463-
// Search for both .resx AND JSON files in workspace
463+
// Search for all supported resource file types
464464
const excludePattern = '{**/.lrm/**,**/.backup/**,**/node_modules/**,**/bin/**,**/obj/**,**/.git/**,**/.vscode/**,**/.github/**,**/.idea/**,**/.vs/**,**/dist/**,**/build/**,**/.devcontainer/**,**/.claude/**}';
465465

466-
const [resxFiles, jsonFiles] = await Promise.all([
466+
const [resxFiles, jsonFiles, androidFiles, iosFiles] = await Promise.all([
467467
vscode.workspace.findFiles('**/*.resx', excludePattern, 10),
468-
vscode.workspace.findFiles('**/*.json', excludePattern, 50) // More results for filtering
468+
vscode.workspace.findFiles('**/*.json', excludePattern, 50),
469+
vscode.workspace.findFiles('**/res/values*/strings.xml', excludePattern, 10),
470+
vscode.workspace.findFiles('**/*.lproj/*.strings', excludePattern, 10)
469471
]);
470472

471473
// Filter RESX files to exclude hidden directories (backup folders, etc.)
@@ -482,22 +484,50 @@ export class LrmService implements vscode.Disposable {
482484
return culture !== null || baseName.length > 0;
483485
});
484486

487+
// Filter Android files - must be in res/values* folder
488+
const filteredAndroidFiles = androidFiles.filter(f => {
489+
const dir = path.dirname(f.fsPath);
490+
return /[/\\]res[/\\]values(-[a-z]{2}(-[A-Z]{2})?)?$/i.test(dir);
491+
});
492+
493+
// Filter iOS files - must be in *.lproj folder
494+
const filteredIosFiles = iosFiles.filter(f => {
495+
const dir = path.dirname(f.fsPath);
496+
return /\.lproj$/i.test(dir);
497+
});
498+
485499
// Group files by directory to find resource directories
486500
const jsonDirs = new Set(validJsonResources.map(f => path.dirname(f.fsPath)));
487501
const resxDirs = new Set(filteredResxFiles.map(f => path.dirname(f.fsPath)));
502+
// For Android, get the parent "res" directory
503+
const androidDirs = new Set(filteredAndroidFiles.map(f => {
504+
const valuesDir = path.dirname(f.fsPath);
505+
return path.dirname(valuesDir); // Go up from values/ to res/
506+
}));
507+
// For iOS, get the parent directory containing .lproj folders
508+
const iosDirs = new Set(filteredIosFiles.map(f => {
509+
const lprojDir = path.dirname(f.fsPath);
510+
return path.dirname(lprojDir); // Go up from xx.lproj/ to parent
511+
}));
488512

489513
const hasResx = filteredResxFiles.length > 0;
490514
const hasJson = validJsonResources.length > 0;
515+
const hasAndroid = filteredAndroidFiles.length > 0;
516+
const hasIos = filteredIosFiles.length > 0;
517+
518+
const formatCount = [hasResx, hasJson, hasAndroid, hasIos].filter(Boolean).length;
491519

492-
if (!hasResx && !hasJson) {
520+
if (formatCount === 0) {
493521
return null;
494522
}
495523

496-
if (hasResx && hasJson) {
497-
// BOTH formats found - ask user which to use
524+
if (formatCount > 1) {
525+
// Multiple formats found - ask user which to use
498526
const choice = await this.promptFormatChoice(
499527
Array.from(resxDirs),
500-
Array.from(jsonDirs)
528+
Array.from(jsonDirs),
529+
Array.from(androidDirs),
530+
Array.from(iosDirs)
501531
);
502532

503533
if (!choice || choice.format === null) {
@@ -511,8 +541,21 @@ export class LrmService implements vscode.Disposable {
511541
if (hasResx) {
512542
return path.dirname(filteredResxFiles[0].fsPath);
513543
}
544+
if (hasJson) {
545+
return path.dirname(validJsonResources[0].fsPath);
546+
}
547+
if (hasAndroid) {
548+
// Return the res/ directory (parent of values/)
549+
const valuesDir = path.dirname(filteredAndroidFiles[0].fsPath);
550+
return path.dirname(valuesDir);
551+
}
552+
if (hasIos) {
553+
// Return the parent directory containing .lproj folders
554+
const lprojDir = path.dirname(filteredIosFiles[0].fsPath);
555+
return path.dirname(lprojDir);
556+
}
514557

515-
return path.dirname(validJsonResources[0].fsPath);
558+
return null;
516559
}
517560

518561
/**
@@ -709,14 +752,16 @@ export class LrmService implements vscode.Disposable {
709752
}
710753

711754
/**
712-
* Prompts user to choose between RESX and JSON when both are found
755+
* Prompts user to choose between multiple resource formats
713756
*/
714757
private async promptFormatChoice(
715758
resxDirs: string[],
716-
jsonDirs: string[]
717-
): Promise<{ format: 'resx' | 'json' | null; selectedDir?: string }> {
759+
jsonDirs: string[],
760+
androidDirs: string[] = [],
761+
iosDirs: string[] = []
762+
): Promise<{ format: 'resx' | 'json' | 'android' | 'ios' | null; selectedDir?: string }> {
718763
interface FormatQuickPickItem extends vscode.QuickPickItem {
719-
format: 'resx' | 'json';
764+
format: 'resx' | 'json' | 'android' | 'ios';
720765
dir: string;
721766
}
722767

@@ -725,7 +770,7 @@ export class LrmService implements vscode.Disposable {
725770
// Add RESX options
726771
for (const dir of resxDirs) {
727772
items.push({
728-
label: '$(file-code) RESX',
773+
label: '$(file-code) RESX (.NET)',
729774
description: path.basename(dir),
730775
detail: dir,
731776
format: 'resx',
@@ -746,6 +791,28 @@ export class LrmService implements vscode.Disposable {
746791
});
747792
}
748793

794+
// Add Android options
795+
for (const dir of androidDirs) {
796+
items.push({
797+
label: '$(device-mobile) Android (strings.xml)',
798+
description: path.basename(dir),
799+
detail: dir,
800+
format: 'android',
801+
dir: dir
802+
});
803+
}
804+
805+
// Add iOS options
806+
for (const dir of iosDirs) {
807+
items.push({
808+
label: '$(device-mobile) iOS (.strings)',
809+
description: path.basename(dir),
810+
detail: dir,
811+
format: 'ios',
812+
dir: dir
813+
});
814+
}
815+
749816
const selected = await vscode.window.showQuickPick(items, {
750817
title: 'Multiple resource formats found',
751818
placeHolder: 'Select which resource folder to use',

vscode-extension/src/extension.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,9 +327,11 @@ function setupEventListeners(context: vscode.ExtensionContext, enableRealtimeSca
327327

328328
outputChannel.appendLine('=== Extension activation complete ===');
329329

330-
// Listen for resource file changes (.resx and JSON)
330+
// Listen for resource file changes (.resx, JSON, Android, iOS)
331331
const resxWatcher = vscode.workspace.createFileSystemWatcher('**/*.resx');
332332
const jsonWatcher = vscode.workspace.createFileSystemWatcher('**/*.json');
333+
const androidWatcher = vscode.workspace.createFileSystemWatcher('**/res/values*/strings.xml');
334+
const iosWatcher = vscode.workspace.createFileSystemWatcher('**/*.lproj/*.strings');
333335
const lrmConfigWatcher = vscode.workspace.createFileSystemWatcher('**/lrm.json');
334336

335337
// Helper to handle resource file changes
@@ -385,6 +387,16 @@ function setupEventListeners(context: vscode.ExtensionContext, enableRealtimeSca
385387
}
386388
});
387389

390+
// Android file watchers (strings.xml in res/values* folders)
391+
androidWatcher.onDidChange(handleResourceChange);
392+
androidWatcher.onDidCreate(handleResourceChange);
393+
androidWatcher.onDidDelete(handleResourceChange);
394+
395+
// iOS file watchers (.strings files in .lproj folders)
396+
iosWatcher.onDidChange(handleResourceChange);
397+
iosWatcher.onDidCreate(handleResourceChange);
398+
iosWatcher.onDidDelete(handleResourceChange);
399+
388400
// lrm.json config file watcher - triggers full refresh when format config changes
389401
lrmConfigWatcher.onDidChange(async () => {
390402
outputChannel.appendLine('lrm.json config changed, refreshing...');
@@ -410,7 +422,7 @@ function setupEventListeners(context: vscode.ExtensionContext, enableRealtimeSca
410422
await handleResourceChange();
411423
});
412424

413-
context.subscriptions.push(resxWatcher, jsonWatcher, lrmConfigWatcher);
425+
context.subscriptions.push(resxWatcher, jsonWatcher, androidWatcher, iosWatcher, lrmConfigWatcher);
414426

415427
// Scan visible editors on activation
416428
vscode.window.visibleTextEditors.forEach(editor => {

0 commit comments

Comments
 (0)