UNPKG

intl-watcher

Version:

Automated translation key extraction and dictionary management plugin for Next.js

185 lines (184 loc) 7.16 kB
import { Node, SyntaxKind } from "ts-morph"; import { NEXT_INTL_GET_TRANSLATIONS_LOCALE, NEXT_INTL_GET_TRANSLATIONS_NAMESPACE } from "./constants.js"; import { printDiagnostic, Severity } from "./diagnostics.js"; function extractTranslationKeysFromProject(project, options) { const sourceFiles = project.getSourceFiles(); const clientTranslationKeys = []; const serverTranslationKeys = []; const clientFunctions = options.applyPartitioning ? [options.partitioningOptions.clientFunction] : []; const serverFunctions = options.applyPartitioning ? [options.partitioningOptions.serverFunction] : options.translationFunctions; for (const sourceFile of sourceFiles) { const variableDeclarations = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration); for (const variableDeclaration of variableDeclarations) { const translationAlias = variableDeclaration.getName(); for (const fnName of clientFunctions) { clientTranslationKeys.push( ...extractTranslationKeysForAlias( variableDeclaration, translationAlias, fnName, TranslationCallMode.Client ) ); } for (const fnName of serverFunctions) { serverTranslationKeys.push( ...extractTranslationKeysForAlias( variableDeclaration, translationAlias, fnName, TranslationCallMode.Server ) ); } } } return [ Array.from(new Set(clientTranslationKeys)).toSorted(), Array.from(new Set(serverTranslationKeys)).toSorted() ]; } function extractTranslationKeysForAlias(variableDeclaration, translationAlias, translationFunction, mode) { const result = isTranslationAliasDeclaration(variableDeclaration, translationFunction, mode); if (result.valid) { const translationKeys = extractTranslationKeysFromSourceFile( variableDeclaration.getSourceFile(), translationAlias ); return translationKeys.map((key) => result.namespace ? `${result.namespace}.${key}` : key); } return []; } const TranslationCallMode = { Client: "Client", Server: "Server" }; function isTranslationAliasDeclaration(variableDeclaration, expectedTranslationAlias, mode) { let currentExpression = variableDeclaration.getInitializer(); if (currentExpression === void 0) { return { valid: false }; } while (Node.isAwaitExpression(currentExpression)) { currentExpression = currentExpression.getExpression(); } if (!Node.isCallExpression(currentExpression)) { return { valid: false }; } const callee = currentExpression.getExpression(); if (!Node.isIdentifier(callee) || callee.getText() !== expectedTranslationAlias) { return { valid: false }; } const args = currentExpression.getArguments(); if (args.length === 0) { return { valid: true }; } const argument = args[0]; let resolvedNamespace = resolveStringLiteral(argument); if (resolvedNamespace) { return { valid: true, namespace: resolvedNamespace }; } if (!(mode === TranslationCallMode.Server && Node.isObjectLiteralExpression(argument) && Node.isPropertyAssignment(argument.getProperty(NEXT_INTL_GET_TRANSLATIONS_LOCALE)))) { printDiagnostic( argument, variableDeclaration.getParentOrThrow(), Severity.Error, "A dynamic namespace value was provided instead of a literal string.", "For reliable extraction of translation keys, please ensure that the namespace is defined", "as a static string literal (or a variable that unequivocally resolves to one)." ); return { valid: false }; } const namespaceProperty = argument.getProperty(NEXT_INTL_GET_TRANSLATIONS_NAMESPACE); if (!(namespaceProperty && Node.isPropertyAssignment(namespaceProperty))) { return { valid: true }; } const initializer = namespaceProperty.getInitializer(); if (initializer) { resolvedNamespace = resolveStringLiteral(initializer); } return resolvedNamespace ? { valid: true, namespace: resolvedNamespace } : { valid: true }; } function resolveStringLiteral(node) { const type = node.getType(); if (type.isStringLiteral()) { return String(type.getLiteralValue()); } } function extractTranslationKeysFromSourceFile(sourceFile, translationAlias) { const translationKeys = []; const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).filter((it) => isTranslationCall(it, translationAlias)); for (const callExpression of callExpressions) { const args = callExpression.getArguments(); if (args.length === 0) { continue; } const firstArgument = args[0]; if (Node.isExpression(firstArgument)) { translationKeys.push(...extractTranslationKeysFromExpression(firstArgument)); } } return translationKeys; } function isTranslationCall(callExpression, expectedTranslationAlias) { const expression = callExpression.getExpression(); if (Node.isIdentifier(expression) && expression.getText() === expectedTranslationAlias) { return true; } if (Node.isPropertyAccessExpression(expression)) { const objectNode = expression.getExpression(); const propertyName = expression.getName(); if (objectNode.getText() === expectedTranslationAlias && propertyName === "rich") { return true; } } return false; } function extractTranslationKeysFromExpression(expression) { if (Node.isStringLiteral(expression)) { return [expression.getLiteralText()]; } if (Node.isIdentifier(expression)) { return extractLiteralValuesFromExpression(expression); } if (Node.isTemplateExpression(expression)) { return extractTranslationKeysFromTemplateLiteral(expression); } if (Node.isPropertyAccessExpression(expression)) { return extractLiteralValuesFromExpression(expression); } printDiagnostic( expression, expression.getParentOrThrow(), Severity.Warn, `Unsupported expression of kind ${expression.getKindName()} detected.`, "This syntax is not currently supported. If you need support for it, please open a feature request", "detailing the syntax kind and the entire expression. Submit your request here:", "https://github.com/ChristianIvicevic/intl-watcher/issues/new?template=03-feature.yml" ); return []; } function extractLiteralValuesFromExpression(expression) { const type = expression.getType(); if (type.isStringLiteral()) { return [String(type.getLiteralValue())]; } if (type.isUnion()) { return type.getUnionTypes().map((t) => t.getLiteralValue()).filter((value) => typeof value === "string"); } return []; } function extractTranslationKeysFromTemplateLiteral(templateExpression) { const translationKeys = []; const head = templateExpression.getHead().getLiteralText(); for (const span of templateExpression.getTemplateSpans()) { const expressionKeys = extractTranslationKeysFromExpression(span.getExpression()); const suffix = span.getLiteral().getLiteralText(); for (const value of expressionKeys) { translationKeys.push(`${head}${value}${suffix}`); } } return translationKeys; } export { extractTranslationKeysFromProject };