@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 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": "65.17.3",
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
@@ -0,0 +1,4 @@
1
+ // Node10-compatible proxy for tsserver plugin resolution.
2
+ // tsserver's tryResolveJSModuleWorker hardcodes Node10 resolution,
3
+ // which ignores package.json "exports" maps.
4
+ module.exports = require('./out/src/hover/tsPlugin');