@hi18n/ts-plugin
Version:
Message internationalization meets immutability and type-safety - TypeScript language server plugin
155 lines • 6.92 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
function init(modules) {
const ts = modules.typescript;
function create(info) {
info.project.projectService.logger.info("Setting up @hi18n/ts-plugin");
const localesToShow = info.config.locales || ["en"];
// Set up decorator object
const proxy = setupProxy(info.languageService);
proxy.getQuickInfoAtPosition = (fileName, position) => {
const prior = info.languageService.getQuickInfoAtPosition(fileName, position);
if (prior)
return prior;
const program = info.languageService.getProgram();
if (!program)
return undefined;
const sourceFile = program.getSourceFile(fileName);
if (!sourceFile)
return undefined;
// `"example/greeting"` in `t("example/greeting")`
const targetNode = findNode(ts, sourceFile, position);
if (!targetNode)
return undefined;
const candidates = resolveTranslation(program, sourceFile, targetNode);
if (!candidates)
return undefined;
return {
kind: ts.ScriptElementKind.string,
kindModifiers: "",
textSpan: {
start: targetNode.getStart(),
length: targetNode.getEnd() - targetNode.getStart(),
},
documentation: [
...candidates.map((c) => ({
text: `${c.message} (${c.locale})\n`,
kind: "text",
})),
],
};
};
function resolveTranslation(program, sourceFile, targetNode) {
// Check if it is in the form of `t("example/greeting", ...)`
if (!ts.isStringLiteral(targetNode))
return undefined;
if (!ts.isCallExpression(targetNode.parent))
return undefined;
if (targetNode.parent.arguments[0] !== targetNode)
return undefined;
// TODO: escape
const translationId = targetNode.getText().replaceAll(/^"|"$/g, "");
// The `t` in `t("example/greeting")`
const tNode = targetNode.parent.expression;
if (!ts.isIdentifier(tNode))
return undefined;
const typeChecker = program.getTypeChecker();
// The `t` function's type. Should be `CompoundTranslatorFunction<Vocabulary>`.
const tType = typeChecker.getTypeAtLocation(tNode);
if (!tType.aliasSymbol || !tType.aliasTypeArguments)
return undefined;
// Check if it is `CompoundTranslatorFunction<...>`.
const name = tType.aliasSymbol.getName();
if (name !== "CompoundTranslatorFunction")
return undefined;
// Extract the `Vocabulary` type
if (tType.aliasTypeArguments.length < 1)
return undefined;
const vocabularyType = tType.aliasTypeArguments[0];
// Look into the definition of Vocabulary. Fetch the corresponding property signature
// Like `"example/greeting": msg("Hello, world!")` in `type Vocabulary = { ... }`
const translationSymbol = typeChecker.getPropertyOfType(vocabularyType, translationId);
if (!translationSymbol)
return undefined;
const translationDecl = (translationSymbol.getDeclarations() ?? [])[0];
if (!translationDecl)
return undefined;
// Find definitions of the messages
const messages = info.languageService.getImplementationAtPosition(translationDecl.getSourceFile().fileName, translationDecl.getStart());
if (!messages)
return undefined;
const foundCandidates = [];
for (const msgdef of messages) {
const sourceFile = program.getSourceFile(msgdef.fileName);
if (!sourceFile)
continue;
const msgdefKeyNode = findNode(ts, sourceFile, msgdef.textSpan.start);
if (!msgdefKeyNode)
continue;
if (!ts.isStringLiteral(msgdefKeyNode))
continue;
const msgdefPropertyNode = msgdefKeyNode.parent;
if (!ts.isPropertyAssignment(msgdefPropertyNode))
continue;
if (msgdefPropertyNode.name !== msgdefKeyNode)
continue;
const msgdefValueNode = msgdefPropertyNode.initializer;
let messageTextNode = msgdefValueNode;
if (ts.isCallExpression(msgdefValueNode) &&
msgdefValueNode.arguments.length === 1) {
messageTextNode = msgdefValueNode.arguments[0];
}
if (!ts.isStringLiteral(messageTextNode))
continue;
let locale = "unknown";
const localeMatch = /([^/]+)\.[mc]?[tj]sx?$/.exec(messageTextNode.getSourceFile().fileName);
if (localeMatch && localeMatch[1] !== "index") {
locale = localeMatch[1];
}
foundCandidates.push({
locale,
message: messageTextNode.text,
});
}
if (foundCandidates.length == 0)
return undefined;
const filteredCandidates = [];
for (const locale of localesToShow) {
const c = foundCandidates.find((c) => c.locale === locale);
if (c) {
filteredCandidates.push(c);
}
}
if (filteredCandidates.length === 0) {
filteredCandidates.push(foundCandidates[0]);
}
return filteredCandidates;
}
return proxy;
}
return { create };
}
function setupProxy(original) {
const proxy = Object.create(null);
for (const [k, v] of Object.entries(original)) {
if (typeof v === "function") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return
proxy[k] = (...args) => v.apply(original, args);
}
else {
proxy[k] = v;
}
}
return proxy;
}
// https://github.com/microsoft/typescript-template-language-service-decorator/blob/2.3.1/src/nodes.ts#L16-L27
function findNode(typescript, sourceFile, position) {
function find(node) {
if (position >= node.getStart() && position < node.getEnd()) {
return typescript.forEachChild(node, find) || node;
}
}
return find(sourceFile);
}
exports.default = init;
//# sourceMappingURL=index.js.map