Skip to content

Commit 8631e71

Browse files
committed
feat: cache keys for optimize searching
1 parent 8f604e7 commit 8631e71

11 files changed

+130
-48
lines changed

README.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060

6161
## Future TODO
6262

63-
- [ ] Cache keys and refresh caching when files change.
63+
- [x] Cache keys and refresh caching when files change "Optimize Search".
6464
- [ ] Support multiple translation APIs (Google, DeepL, Microsoft).
6565
- [ ] Allow fallback behavior if the preferred API fails.
6666
- [ ] Custom key formats when created allow options like snake_case, camelCase, kebab-case.
@@ -76,7 +76,6 @@
7676
- [ ] Search for keys or values that contain a specific string.
7777
- [ ] Auto-merge similar keys to reduce redundancy.
7878
- [ ] Suggest existing keys for reuse when adding new ones.
79-
- [ ] Optimized search for translation keys in large datasets.
8079
- [ ] Rename keys and update references in code automatically.
8180
- [ ] Detect and clean up unused translation keys automatically.
8281

src/commands/addKey.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as vscode from 'vscode';
22
import { printChannelOutput } from './../extension';
33
import { JsonParser } from '../utils/json-parser';
44
import { getTranslationFromCopilot } from '../utils/translationUtils';
5-
import { JsonI18nKeySettings } from '../models/settings';
5+
import { JsonI18nKeySettings } from '../models/JsonI18nKeySettings';
66

77
async function addKeyCommand(): Promise<void> {
88
const editor = vscode.window.activeTextEditor;
@@ -11,7 +11,7 @@ async function addKeyCommand(): Promise<void> {
1111
}
1212

1313
let keyPath = undefined;
14-
const settings = vscode.workspace.getConfiguration('json-i18n-key') as unknown as JsonI18nKeySettings;
14+
const settings = JsonI18nKeySettings.instance;
1515
if (settings.typeOfGetKey === 'Manual') {
1616
keyPath = await vscode.window.showInputBox({ prompt: 'Enter Key Path:' });
1717
} else if (settings.typeOfGetKey === 'Clipboard') {

src/commands/checkKey.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as vscode from 'vscode';
22
import { checkExistKey } from '../utils/jsonUtils';
3+
import { JsonI18nKeySettings } from '../models/JsonI18nKeySettings';
34

45
async function checkExistKeyCommand(): Promise<void> {
56
const editor = vscode.window.activeTextEditor;
@@ -8,8 +9,7 @@ async function checkExistKeyCommand(): Promise<void> {
89
}
910

1011
let keyPath = undefined;
11-
const settings = vscode.workspace.getConfiguration('json-i18n-key');
12-
const translationFiles: { filePath: string, lang: string; }[] = settings.get('translationFiles', []);
12+
const settings = JsonI18nKeySettings.instance;
1313

1414
if (settings.typeOfGetKey === 'Manual') {
1515
keyPath = await vscode.window.showInputBox({ prompt: 'Enter Key Path:' });
@@ -35,7 +35,7 @@ async function checkExistKeyCommand(): Promise<void> {
3535
return;
3636
}
3737

38-
for (const translationFile of translationFiles) {
38+
for (const translationFile of settings.translationFiles) {
3939
if (!translationFile.filePath) {
4040
vscode.window.showInformationMessage(`File path is required for ${translationFile.lang}`);
4141
continue;

src/commands/findKey.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as vscode from 'vscode';
22
import { findKeyPosition } from '../utils/jsonUtils';
3+
import { JsonI18nKeySettings } from '../models/JsonI18nKeySettings';
34

45
async function findKeyCommand(): Promise<void> {
56
const editor = vscode.window.activeTextEditor;
@@ -9,8 +10,7 @@ async function findKeyCommand(): Promise<void> {
910

1011
const document = editor.document;
1112
let keyPath = undefined;
12-
const settings = vscode.workspace.getConfiguration('json-i18n-key');
13-
const translationFiles: { filePath: string, lang: string; }[] = settings.get('translationFiles', []);
13+
const settings = JsonI18nKeySettings.instance;
1414

1515
if (document.languageId === 'json') {
1616
keyPath = await vscode.window.showInputBox({ prompt: 'Enter Key Path:' });
@@ -40,7 +40,7 @@ async function findKeyCommand(): Promise<void> {
4040
return;
4141
}
4242

43-
for (const translationFile of translationFiles) {
43+
for (const translationFile of settings.translationFiles) {
4444
if (!translationFile.filePath) {
4545
vscode.window.showErrorMessage(`Translation file path for ${translationFile.lang} does not exist`);
4646
continue;

src/commands/removeKey.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from 'vscode';
22
import { JsonParser } from '../utils/json-parser';
3-
import { JsonI18nKeySettings } from '../models/settings';
3+
import { JsonI18nKeySettings } from '../models/JsonI18nKeySettings';
44

55
async function removeKeyCommand(): Promise<void> {
66
const editor = vscode.window.activeTextEditor;
@@ -9,7 +9,7 @@ async function removeKeyCommand(): Promise<void> {
99
}
1010

1111
let keyPath = undefined;
12-
const settings = vscode.workspace.getConfiguration('json-i18n-key') as unknown as JsonI18nKeySettings;
12+
const settings = JsonI18nKeySettings.instance;
1313

1414
if (settings.typeOfGetKey === 'Manual') {
1515
keyPath = await vscode.window.showInputBox({ prompt: 'Enter Key Path:' });

src/commands/renameKey.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from 'vscode';
22
import { JsonParser } from '../utils/json-parser';
3-
import { JsonI18nKeySettings } from '../models/settings';
3+
import { JsonI18nKeySettings } from '../models/JsonI18nKeySettings';
44

55
async function renameKeyCommand(): Promise<void> {
66
const editor = vscode.window.activeTextEditor;
@@ -10,7 +10,7 @@ async function renameKeyCommand(): Promise<void> {
1010

1111
let keyPath = undefined;
1212
let newKey = undefined;
13-
const settings = vscode.workspace.getConfiguration('json-i18n-key') as unknown as JsonI18nKeySettings;
13+
const settings = JsonI18nKeySettings.instance;
1414

1515
if (settings.typeOfGetKey === 'Manual') {
1616
keyPath = await vscode.window.showInputBox({ prompt: 'Enter Key Path:' });

src/commands/updateKey.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from 'vscode';
22
import { JsonParser } from '../utils/json-parser';
3-
import { JsonI18nKeySettings } from '../models/settings';
3+
import { JsonI18nKeySettings } from '../models/JsonI18nKeySettings';
44

55
async function updateKeyCommand(): Promise<void> {
66
const editor = vscode.window.activeTextEditor;
@@ -10,7 +10,7 @@ async function updateKeyCommand(): Promise<void> {
1010

1111
let keyPath = undefined;
1212
let newValue = undefined;
13-
const settings = vscode.workspace.getConfiguration('json-i18n-key') as unknown as JsonI18nKeySettings;
13+
const settings = JsonI18nKeySettings.instance;
1414

1515

1616
if (settings.typeOfGetKey === 'Manual') {

src/extension.ts

+48-12
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import { checkExistKeyCommand } from './commands/checkKey';
55
import { renameKeyCommand } from './commands/renameKey';
66
import { removeKeyCommand } from './commands/removeKey';
77
import { updateKeyCommand } from './commands/updateKey';
8-
import { getHoverTranslation, getKeysValues } from './utils/jsonUtils';
8+
import { getHoverTranslation, loadKeys } from './utils/jsonUtils';
9+
import { JsonI18nKeySettings } from './models/JsonI18nKeySettings';
910

1011
let outputChannel: vscode.OutputChannel | undefined;
12+
let keyCache: string[] = [];
1113

1214
/**
1315
* This method is called when the extension is activated.
@@ -49,23 +51,56 @@ export function activate(context: vscode.ExtensionContext): void {
4951
}
5052

5153
let fullKeyPath = document.getText(range);
54+
// if (context.triggerKind !== vscode.CompletionTriggerKind.TriggerCharacter) {
55+
const textBeforeCursor = document.getText(new vscode.Range(range.start, position));
56+
fullKeyPath = textBeforeCursor;
57+
// }
5258
// Remove surrounding quotes (single or double quotes) if they exist
5359
fullKeyPath = fullKeyPath.replace(/^['"]|['"]$/g, '');
5460

55-
const results = getKeysValues(fullKeyPath);
56-
if (results.length === 0) {
57-
return;
58-
}
59-
60-
return results.map((obj:any) => {
61-
const completionItem = new vscode.CompletionItem(obj.parentProperty, vscode.CompletionItemKind.Field);
62-
completionItem.documentation = new vscode.MarkdownString(obj.value);
61+
const uniqueKeys = Array.from(
62+
new Set( keyCache
63+
.filter(key => key.startsWith(fullKeyPath))
64+
.map((key: string) => {
65+
const str = key.slice(fullKeyPath.lastIndexOf('.') + 1, key.length);
66+
if (str.indexOf('.') === -1) {
67+
return str;
68+
}
69+
const nextKey = str.slice(0, str.indexOf('.'));
70+
return nextKey;
71+
})
72+
)
73+
);
74+
return uniqueKeys.map((key: string) => {
75+
const completionItem = new vscode.CompletionItem(key, vscode.CompletionItemKind.Field);
76+
completionItem.documentation = new vscode.MarkdownString(key);
77+
completionItem.detail = "i18n key";
6378
return completionItem;
6479
});
65-
}
66-
}
80+
},
81+
},
82+
'.' // Trigger on `.`
6783
);
6884

85+
86+
// Set up file watcher for en.json
87+
const watcher = vscode.workspace.createFileSystemWatcher(JsonI18nKeySettings.instance.enJsonFilePath);
88+
watcher.onDidChange(() => {
89+
console.log('en.json changed, reloading keys...');
90+
keyCache = loadKeys();
91+
});
92+
watcher.onDidCreate(() => {
93+
console.log('en.json created, loading keys...');
94+
keyCache = loadKeys();
95+
});
96+
watcher.onDidDelete(() => {
97+
console.log('en.json deleted, clearing cache...');
98+
keyCache = [];
99+
});
100+
101+
// Load keys initially
102+
keyCache = loadKeys();
103+
69104
// Register commands and other providers
70105
context.subscriptions.push(
71106
vscode.commands.registerCommand('json-i18n-key.findKey', findKeyCommand),
@@ -75,7 +110,8 @@ export function activate(context: vscode.ExtensionContext): void {
75110
vscode.commands.registerCommand('json-i18n-key.updateKey', updateKeyCommand),
76111
vscode.commands.registerCommand('json-i18n-key.addKey', addKeyCommand),
77112
hoverProvider,
78-
completionProvider
113+
completionProvider,
114+
watcher,
79115
);
80116
}
81117

src/models/JsonI18nKeySettings.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as vscode from 'vscode';
2+
3+
export class JsonI18nKeySettings {
4+
private static _instance: JsonI18nKeySettings;
5+
6+
translationFiles: TranslationFile[] = [];
7+
typeOfGetKey: 'Selection' | 'Clipboard' | 'Manual';
8+
enableCopilotTranslation: boolean = false;
9+
preserveFormating: boolean = false;
10+
enJsonFilePath: string = '';
11+
12+
private constructor() {
13+
const settings = vscode.workspace.getConfiguration('json-i18n-key') as unknown as Settings;
14+
this.translationFiles = settings.translationFiles;
15+
this.typeOfGetKey = settings.typeOfGetKey;
16+
this.enableCopilotTranslation = settings.enableCopilotTranslation;
17+
this.preserveFormating = settings.preserveFormating;
18+
this.enJsonFilePath = settings.translationFiles.find(file => file.lang === 'en' && file.isDefault == true)?.filePath || '';
19+
}
20+
21+
static get instance(): JsonI18nKeySettings {
22+
if (!JsonI18nKeySettings._instance) {
23+
JsonI18nKeySettings._instance = new JsonI18nKeySettings();
24+
}
25+
return JsonI18nKeySettings._instance;
26+
}
27+
}
28+
29+
30+
interface Settings {
31+
translationFiles: TranslationFile[];
32+
typeOfGetKey: 'Selection' | 'Clipboard' | 'Manual';
33+
enableCopilotTranslation: boolean;
34+
preserveFormating: boolean;
35+
}
36+
37+
interface TranslationFile {
38+
lang: string;
39+
filePath: string;
40+
isDefault: boolean;
41+
}

src/models/settings.ts

-12
This file was deleted.

src/utils/jsonUtils.ts

+26-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { JsonI18nKeySettings } from './../models/settings';
1+
import { JsonI18nKeySettings } from '../models/JsonI18nKeySettings';
22
import { JsonObject } from '../models/jsonObject';
33
import * as vscode from 'vscode';
44
import { loadJsonFileSync } from './fileUtils';
@@ -17,13 +17,11 @@ export function getKeyValue(jsonFilePath: string, keyPath: string): string {
1717
}
1818

1919
export function getKeysValues(keyPath: string): string[] {
20-
const settings = vscode.workspace.getConfiguration('json-i18n-key') as unknown as JsonI18nKeySettings;
21-
const enFile = settings.translationFiles.find(file => file.lang === 'en' && file.isDefault == true);
22-
if (!enFile || enFile.filePath === '') {
20+
if (JsonI18nKeySettings.instance.enJsonFilePath === '') {
2321
printChannelOutput('English translation file not found');
2422
return [];
2523
}
26-
const jsonData = loadJsonFileSync(enFile.filePath);
24+
const jsonData = loadJsonFileSync(JsonI18nKeySettings.instance.enJsonFilePath);
2725
// const parentKey = keyPath.split('.').slice(0, -1).join('.');
2826
const keys = keyPath.split('.');
2927
const lastKey = keys.pop() as string;
@@ -37,10 +35,9 @@ export function getKeysValues(keyPath: string): string[] {
3735
}
3836

3937
export function getHoverTranslation(keyPath: string): vscode.MarkdownString {
40-
const settings = vscode.workspace.getConfiguration('json-i18n-key') as unknown as JsonI18nKeySettings;
4138
const hoverMessage = new vscode.MarkdownString();
4239
hoverMessage.appendMarkdown(`**Key:** \`${keyPath}\`\n\n`);
43-
for (const translationFile of settings.translationFiles) {
40+
for (const translationFile of JsonI18nKeySettings.instance.translationFiles) {
4441
if (translationFile.filePath) {
4542
const keyValue = getKeyValue(translationFile.filePath, keyPath);
4643
hoverMessage.appendMarkdown(`**${translationFile.lang.toUpperCase()}:** ${keyValue || 'N/A'}\n\n`);
@@ -98,4 +95,25 @@ export function getOrCreateParentObject(jsonData: JsonObject, keyPath: string):
9895

9996
export function isJsonObject(obj: unknown): obj is JsonObject {
10097
return obj !== null && typeof obj === 'object';
101-
}
98+
}
99+
100+
export function loadKeys() {
101+
if (JsonI18nKeySettings.instance.enJsonFilePath === '') {
102+
printChannelOutput('English translation file not found');
103+
return [];
104+
}
105+
106+
return flattenKeys(loadJsonFileSync(JsonI18nKeySettings.instance.enJsonFilePath));
107+
}
108+
109+
function flattenKeys(obj: any, prefix = ''): string[] {
110+
return Object.keys(obj).reduce((acc, key) => {
111+
const path = prefix ? `${prefix}.${key}` : key;
112+
if (typeof obj[key] === 'object' && obj[key] !== null) {
113+
acc.push(...flattenKeys(obj[key], path));
114+
} else {
115+
acc.push(path);
116+
}
117+
return acc;
118+
}, [] as string[]);
119+
}

0 commit comments

Comments
 (0)