@salesforce/vscode-i18n 65.17.3 → 66.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/out/src/hover/messageCache.d.ts +19 -0
- package/out/src/hover/messageCache.d.ts.map +1 -0
- package/out/src/hover/messageCache.js +110 -0
- package/out/src/hover/tsPlugin.d.ts +8 -0
- package/out/src/hover/tsPlugin.d.ts.map +1 -0
- package/out/src/hover/tsPlugin.js +114 -0
- package/package.json +6 -1
- package/tsPlugin.js +4 -0
package/README.md
CHANGED
|
@@ -104,6 +104,24 @@ service.messageBundleManager.registerMessageBundle(instanceName, {
|
|
|
104
104
|
});
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
### i18n Hover (TypeScript Language Service Plugin)
|
|
108
|
+
|
|
109
|
+
The package includes a TypeScript plugin that shows message text when you hover over `nls.localize('key')` or `coerceMessageKey('key')` in your editor.
|
|
110
|
+
|
|
111
|
+
**Setup:**
|
|
112
|
+
|
|
113
|
+
1. Add the plugin to your `tsconfig.json`:
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"compilerOptions": {
|
|
117
|
+
"plugins": [{ "name": "@salesforce/vscode-i18n/tsPlugin" }]
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
2. In VS Code, use the workspace TypeScript version: run "TypeScript: Select TypeScript Version" and choose "Use Workspace Version". (Plugins only load with the workspace TS version.)
|
|
122
|
+
|
|
123
|
+
Your messages must follow the standard format: `export const messages = { ... }` in a `messages/i18n.ts` file within the package.
|
|
124
|
+
|
|
107
125
|
### Constants
|
|
108
126
|
|
|
109
127
|
- `DEFAULT_LOCALE`: The default locale ('en')
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type MessageEntry = {
|
|
2
|
+
text: string;
|
|
3
|
+
namePos: number;
|
|
4
|
+
nameEnd: number;
|
|
5
|
+
};
|
|
6
|
+
type MessagesMap = Record<string, MessageEntry>;
|
|
7
|
+
type TsLike = {
|
|
8
|
+
createSourceFile: (path: string, content: string, target: number, setParentNodes: boolean) => {
|
|
9
|
+
statements: Iterable<unknown>;
|
|
10
|
+
};
|
|
11
|
+
SyntaxKind: Record<string, number>;
|
|
12
|
+
};
|
|
13
|
+
export type MessagesResult = {
|
|
14
|
+
i18nPath: string;
|
|
15
|
+
entries: MessagesMap;
|
|
16
|
+
};
|
|
17
|
+
export declare const getMessagesForFile: (ts: TsLike, sourceFilePath: string) => MessagesResult | undefined;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=messageCache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messageCache.d.ts","sourceRoot":"","sources":["../../../src/hover/messageCache.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAC9E,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AAEhD,KAAK,MAAM,GAAG;IACZ,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,KAAK;QAAE,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;IAChI,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC,CAAC;AAuFF,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,CAAC;AAOxE,eAAO,MAAM,kBAAkB,GAC7B,IAAI,MAAM,EACV,gBAAgB,MAAM,KACrB,cAAc,GAAG,SAmBnB,CAAC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025, salesforce.com, inc.
|
|
4
|
+
* All rights reserved.
|
|
5
|
+
* Licensed under the BSD 3-Clause license.
|
|
6
|
+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.getMessagesForFile = void 0;
|
|
10
|
+
const fs = require("node:fs");
|
|
11
|
+
const path = require("node:path");
|
|
12
|
+
const findPackageRoot = (filePath) => {
|
|
13
|
+
let dir = path.dirname(filePath);
|
|
14
|
+
const root = path.parse(filePath).root;
|
|
15
|
+
while (dir !== root) {
|
|
16
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
dir = path.dirname(dir);
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
};
|
|
23
|
+
const findI18nPath = (packageRoot) => {
|
|
24
|
+
const candidates = [
|
|
25
|
+
path.join(packageRoot, 'src', 'messages', 'i18n.ts'),
|
|
26
|
+
path.join(packageRoot, 'messages', 'i18n.ts')
|
|
27
|
+
];
|
|
28
|
+
const found = candidates.find(p => fs.existsSync(p));
|
|
29
|
+
if (found)
|
|
30
|
+
return found;
|
|
31
|
+
const srcDir = path.join(packageRoot, 'src');
|
|
32
|
+
if (!fs.existsSync(srcDir))
|
|
33
|
+
return undefined;
|
|
34
|
+
const match = fs.readdirSync(srcDir, { withFileTypes: true, recursive: true })
|
|
35
|
+
.find(e => e.name === 'i18n.ts' && !e.isDirectory() && path.basename(e.parentPath) === 'messages');
|
|
36
|
+
return match ? path.join(match.parentPath, match.name) : undefined;
|
|
37
|
+
};
|
|
38
|
+
const extractMessagesFromObjectLiteral = (ts, node) => {
|
|
39
|
+
const entries = [];
|
|
40
|
+
for (const prop of node.properties ?? []) {
|
|
41
|
+
const name = prop.name;
|
|
42
|
+
if (!name || typeof name.text !== 'string' || !name.text)
|
|
43
|
+
continue;
|
|
44
|
+
const key = name.text;
|
|
45
|
+
const sk = ts.SyntaxKind;
|
|
46
|
+
// name.pos includes leading trivia; compute actual start from end
|
|
47
|
+
const isQuoted = name.kind === sk.StringLiteral || name.kind === sk.NoSubstitutionTemplateLiteral;
|
|
48
|
+
const nameStart = (name.end ?? 0) - key.length - (isQuoted ? 1 : 0);
|
|
49
|
+
const init = prop.initializer;
|
|
50
|
+
if (!init)
|
|
51
|
+
continue;
|
|
52
|
+
const text = init.kind === sk.StringLiteral || init.kind === sk.NoSubstitutionTemplateLiteral
|
|
53
|
+
? init.text
|
|
54
|
+
: init.kind === sk.TemplateExpression
|
|
55
|
+
? (init.head
|
|
56
|
+
.text +
|
|
57
|
+
init.templateSpans
|
|
58
|
+
.map(s => s.literal.text)
|
|
59
|
+
.join(''))
|
|
60
|
+
: undefined;
|
|
61
|
+
if (text !== undefined) {
|
|
62
|
+
entries.push([key, { text, namePos: nameStart, nameEnd: name.end ?? 0 }]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return Object.fromEntries(entries);
|
|
66
|
+
};
|
|
67
|
+
const extractMessagesFromSource = (ts, sourceFile) => {
|
|
68
|
+
const sk = ts.SyntaxKind;
|
|
69
|
+
for (const stmt of sourceFile.statements) {
|
|
70
|
+
const s = stmt;
|
|
71
|
+
if (s.kind !== sk.VariableStatement)
|
|
72
|
+
continue;
|
|
73
|
+
const hasExport = (s.modifiers ?? []).some((m) => m && typeof m === 'object' && m.kind === sk.ExportKeyword);
|
|
74
|
+
if (!hasExport)
|
|
75
|
+
continue;
|
|
76
|
+
for (const decl of s.declarationList?.declarations ?? []) {
|
|
77
|
+
const d = decl;
|
|
78
|
+
if (d.name?.text !== 'messages' || !d.initializer)
|
|
79
|
+
continue;
|
|
80
|
+
const init = d.initializer;
|
|
81
|
+
const obj = init.kind === sk.AsExpression ? init.expression : init;
|
|
82
|
+
if (obj.kind === sk.ObjectLiteralExpression) {
|
|
83
|
+
return extractMessagesFromObjectLiteral(ts, obj);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {};
|
|
88
|
+
};
|
|
89
|
+
const cache = new Map();
|
|
90
|
+
const getMessagesForFile = (ts, sourceFilePath) => {
|
|
91
|
+
const packageRoot = findPackageRoot(sourceFilePath);
|
|
92
|
+
if (!packageRoot)
|
|
93
|
+
return undefined;
|
|
94
|
+
const i18nPath = findI18nPath(packageRoot);
|
|
95
|
+
if (!i18nPath)
|
|
96
|
+
return undefined;
|
|
97
|
+
const stat = fs.statSync(i18nPath);
|
|
98
|
+
const cached = cache.get(i18nPath);
|
|
99
|
+
if (cached && cached.mtimeMs === stat.mtimeMs)
|
|
100
|
+
return cached.result;
|
|
101
|
+
const content = fs.readFileSync(i18nPath, 'utf8');
|
|
102
|
+
const sourceFile = ts.createSourceFile(i18nPath, content, 99, // ScriptTarget.Latest
|
|
103
|
+
true);
|
|
104
|
+
const entries = extractMessagesFromSource(ts, sourceFile);
|
|
105
|
+
const result = { i18nPath, entries };
|
|
106
|
+
cache.set(i18nPath, { result, mtimeMs: stat.mtimeMs });
|
|
107
|
+
return result;
|
|
108
|
+
};
|
|
109
|
+
exports.getMessagesForFile = getMessagesForFile;
|
|
110
|
+
//# sourceMappingURL=messageCache.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type ts from 'typescript/lib/tsserverlibrary';
|
|
2
|
+
declare function init(modules: {
|
|
3
|
+
typescript: typeof import('typescript/lib/tsserverlibrary');
|
|
4
|
+
}): {
|
|
5
|
+
create: (info: ts.server.PluginCreateInfo) => ts.LanguageService;
|
|
6
|
+
};
|
|
7
|
+
export = init;
|
|
8
|
+
//# sourceMappingURL=tsPlugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tsPlugin.d.ts","sourceRoot":"","sources":["../../../src/hover/tsPlugin.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,MAAM,gCAAgC,CAAC;AAGrD,iBAAS,IAAI,CAAC,OAAO,EAAE;IAAE,UAAU,EAAE,cAAc,gCAAgC,CAAC,CAAA;CAAE;mBAkD9D,EAAE,CAAC,MAAM,CAAC,gBAAgB;EAqEjD;AAED,SAAS,IAAI,CAAC"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025, salesforce.com, inc.
|
|
4
|
+
* All rights reserved.
|
|
5
|
+
* Licensed under the BSD 3-Clause license.
|
|
6
|
+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
7
|
+
*/
|
|
8
|
+
const messageCache_1 = require("./messageCache");
|
|
9
|
+
function init(modules) {
|
|
10
|
+
const ts = modules.typescript;
|
|
11
|
+
const isNlsLocalizeCall = (node) => {
|
|
12
|
+
if (node.kind !== ts.SyntaxKind.CallExpression)
|
|
13
|
+
return false;
|
|
14
|
+
const call = node;
|
|
15
|
+
if (call.expression.kind !== ts.SyntaxKind.PropertyAccessExpression)
|
|
16
|
+
return false;
|
|
17
|
+
const prop = call.expression;
|
|
18
|
+
return (prop.expression.kind === ts.SyntaxKind.Identifier &&
|
|
19
|
+
prop.expression.text === 'nls' &&
|
|
20
|
+
prop.name.text === 'localize');
|
|
21
|
+
};
|
|
22
|
+
const isCoerceMessageKeyCall = (node) => node.kind === ts.SyntaxKind.CallExpression &&
|
|
23
|
+
node.expression.kind === ts.SyntaxKind.Identifier &&
|
|
24
|
+
node.expression.text === 'coerceMessageKey';
|
|
25
|
+
const getKeyAtPosition = (sourceFile, position) => {
|
|
26
|
+
let sf = sourceFile;
|
|
27
|
+
let node = ts.getTokenAtPosition?.(sf, position);
|
|
28
|
+
if (!node)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (!node.parent && sourceFile.text) {
|
|
31
|
+
sf = ts.createSourceFile(sourceFile.fileName, sourceFile.text, sourceFile.languageVersion ?? 99, true);
|
|
32
|
+
node = ts.getTokenAtPosition?.(sf, position);
|
|
33
|
+
if (!node)
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const str = node.kind === ts.SyntaxKind.StringLiteral || node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral
|
|
37
|
+
? node.text
|
|
38
|
+
: undefined;
|
|
39
|
+
if (str === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
const parent = node.parent;
|
|
42
|
+
if (!parent)
|
|
43
|
+
return undefined;
|
|
44
|
+
const isTarget = (isNlsLocalizeCall(parent) || isCoerceMessageKeyCall(parent)) &&
|
|
45
|
+
parent.arguments[0] === node;
|
|
46
|
+
return isTarget ? str : undefined;
|
|
47
|
+
};
|
|
48
|
+
function create(info) {
|
|
49
|
+
const proxy = Object.create(null);
|
|
50
|
+
for (const k of Object.keys(info.languageService)) {
|
|
51
|
+
const x = info.languageService[k];
|
|
52
|
+
proxy[k] = (...args) => x.apply(info.languageService, args);
|
|
53
|
+
}
|
|
54
|
+
const tsLike = {
|
|
55
|
+
createSourceFile: (p, c, t, s) => ts.createSourceFile(p, c, t, s),
|
|
56
|
+
SyntaxKind: ts.SyntaxKind
|
|
57
|
+
};
|
|
58
|
+
const resolveKey = (fileName, position) => {
|
|
59
|
+
const program = info.languageService.getProgram();
|
|
60
|
+
const sourceFile = program?.getSourceFile(fileName);
|
|
61
|
+
if (!program || !sourceFile)
|
|
62
|
+
return undefined;
|
|
63
|
+
const key = getKeyAtPosition(sourceFile, position);
|
|
64
|
+
if (!key)
|
|
65
|
+
return undefined;
|
|
66
|
+
const result = (0, messageCache_1.getMessagesForFile)(tsLike, fileName);
|
|
67
|
+
if (!result || !(key in result.entries))
|
|
68
|
+
return undefined;
|
|
69
|
+
return { key, sourceFile, result };
|
|
70
|
+
};
|
|
71
|
+
proxy.getQuickInfoAtPosition = (fileName, position) => {
|
|
72
|
+
const prior = info.languageService.getQuickInfoAtPosition(fileName, position);
|
|
73
|
+
const resolved = resolveKey(fileName, position);
|
|
74
|
+
if (!resolved)
|
|
75
|
+
return prior;
|
|
76
|
+
const { key, sourceFile, result } = resolved;
|
|
77
|
+
return {
|
|
78
|
+
kind: ts.ScriptElementKind.string,
|
|
79
|
+
kindModifiers: '',
|
|
80
|
+
textSpan: prior?.textSpan ?? {
|
|
81
|
+
start: sourceFile.getPositionOfLineAndCharacter?.(sourceFile.getLineAndCharacterOfPosition(position).line, sourceFile.getLineAndCharacterOfPosition(position).character) ?? position,
|
|
82
|
+
length: key.length + 2
|
|
83
|
+
},
|
|
84
|
+
displayParts: [{ kind: 'text', text: `(i18n) ${key}` }],
|
|
85
|
+
documentation: [{ kind: 'text', text: result.entries[key].text }]
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
proxy.getDefinitionAndBoundSpan = (fileName, position) => {
|
|
89
|
+
const prior = info.languageService.getDefinitionAndBoundSpan(fileName, position);
|
|
90
|
+
const resolved = resolveKey(fileName, position);
|
|
91
|
+
if (!resolved)
|
|
92
|
+
return prior;
|
|
93
|
+
const { key, result } = resolved;
|
|
94
|
+
const entry = result.entries[key];
|
|
95
|
+
const definition = {
|
|
96
|
+
fileName: result.i18nPath,
|
|
97
|
+
textSpan: { start: entry.namePos, length: entry.nameEnd - entry.namePos },
|
|
98
|
+
kind: ts.ScriptElementKind.string,
|
|
99
|
+
name: key,
|
|
100
|
+
containerKind: ts.ScriptElementKind.variableElement,
|
|
101
|
+
containerName: 'messages'
|
|
102
|
+
};
|
|
103
|
+
const priorDefs = prior?.definitions ?? [];
|
|
104
|
+
return {
|
|
105
|
+
definitions: [definition, ...priorDefs],
|
|
106
|
+
textSpan: prior?.textSpan ?? { start: position, length: key.length + 2 }
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
return proxy;
|
|
110
|
+
}
|
|
111
|
+
return { create };
|
|
112
|
+
}
|
|
113
|
+
module.exports = init;
|
|
114
|
+
//# sourceMappingURL=tsPlugin.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/vscode-i18n",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "66.0.0",
|
|
4
4
|
"description": "Internationalization (i18n) library for Salesforce VS Code extensions",
|
|
5
5
|
"author": "Salesforce",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
"homepage": "https://github.com/forcedotcom/salesforcedx-vscode/tree/develop/packages/salesforcedx-vscode-i18n",
|
|
15
15
|
"main": "out/src/index.js",
|
|
16
16
|
"types": "out/src/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./out/src/index.js",
|
|
19
|
+
"./tsPlugin": "./out/src/hover/tsPlugin.js"
|
|
20
|
+
},
|
|
17
21
|
"files": [
|
|
22
|
+
"tsPlugin.js",
|
|
18
23
|
"out/**/*.js",
|
|
19
24
|
"out/**/*.d.ts",
|
|
20
25
|
"out/**/*.d.ts.map"
|
package/tsPlugin.js
ADDED