UNPKG

ts-transform-react-intl

Version:

Extracts string messages for translation from modules that use React Intl.

223 lines (205 loc) 5.76 kB
import * as ts from "typescript"; import { MessageDescriptor } from "./types"; import { _ } from "./macro"; import { interpolateName } from "loader-utils"; export type Extractor = ( msgId: string, messages: MessageDescriptor, filename?: string ) => void; export type InterpolateNameFn = ( sourceFileName: string, msg: MessageDescriptor ) => string; export interface Opts { /** * Override import name (the `_`) in `import {_} from 'ts-transform-react-intl'` * * @type {string} * @memberof Opts */ macroImportName?: string; /** * Override module specifier in `import {_} from 'ts-transform-react-intl` * * @type {string} * @memberof Opts */ macroModuleSpecifier?: string; /** * Callback function that gets called everytime we encountered something * that looks like a MessageDescriptor * * @type {Extractor} * @memberof Opts */ onMsgExtracted?: Extractor; /** * webpack-style name interpolation * * @type {(InterpolateNameFn | string)} * @memberof Opts */ interpolateName?: InterpolateNameFn | string; /** * Base URL of your project, same as your compiler tsconfig.json * This is primarily used to interpolate relative path instead of * absolute path all the time, which varies machine to machine * * @type {string} * @memberof Opts */ baseUrl?: string; } const DEFAULT_OPTS: Opts = { macroImportName: _.name, macroModuleSpecifier: require("../package.json").name, baseUrl: "", onMsgExtracted: () => undefined }; /** * Trim the trailing & beginning ': 'asd' -> asd * * @param {string} txt text * @returns trimmed string */ function trimSingleQuote(txt: string): string { return txt.replace(/["']/g, ""); } /** * Extract the object literal in TS AST into MessageDescriptor * * @param {ts.ObjectLiteralExpression} node object literal * @returns {Messages} */ function extractMessageDescriptor( node: ts.ObjectLiteralExpression, sf: ts.SourceFile, interpolateNameFnOrPattern?: Opts["interpolateName"], baseUrl: string = "" ): MessageDescriptor { const msg: MessageDescriptor = { id: "", description: "", defaultMessage: "" }; // Go thru each property ts.forEachChild(node, (p: ts.PropertyAssignment) => { switch (p.name.getText(sf)) { case "id": const id = trimSingleQuote(p.initializer.getText(sf)); msg.id = id; break; case "description": msg.description = trimSingleQuote(p.initializer.getText(sf)); break; case "defaultMessage": msg.defaultMessage = trimSingleQuote(p.initializer.getText(sf)); break; } }); switch (typeof interpolateNameFnOrPattern) { case "string": msg.id = interpolateName( { sourcePath: sf.fileName.replace(baseUrl, "") } as any, interpolateNameFnOrPattern, { content: JSON.stringify(msg) } ); break; case "function": msg.id = interpolateNameFnOrPattern(sf.fileName, msg); break; } return msg; } function findMacroHook( node: ts.Node, sf: ts.SourceFile, macroImportName: string, macroModuleSpecifier: string ): string { let hook = ""; if ( ts.isImportDeclaration(node) && trimSingleQuote(node.moduleSpecifier.getText(sf)) === macroModuleSpecifier ) { const { namedBindings } = node.importClause; // Search through named bindings to find our macro ts.forEachChild(namedBindings, p => { if (!ts.isImportSpecifier(p)) { return; } // This is a alias, like `import {_ as foo}` if (p.propertyName) { if (p.propertyName.getText(sf) === macroImportName) { hook = p.name.getText(sf); } } else if (p.name.getText(sf) === macroImportName) { hook = p.name.getText(sf); } }); } return hook; } function isMacroExpression( node: ts.Node, sf: ts.SourceFile, hook: string ): node is ts.CallExpression & boolean { // Make sure it's a function call return ( hook && ts.isCallExpression(node) && // Make sure the fn name matches our hook node.expression.getText(sf) === hook && // Make sure we got only 1 arg node.arguments.length === 1 && node.arguments[0] && // Make sure it's a object literal ts.isObjectLiteralExpression(node.arguments[0]) ); } export function transform(opts: Opts) { opts = { ...DEFAULT_OPTS, ...opts }; return (ctx: ts.TransformationContext): ts.Transformer<ts.SourceFile> => { function getVisitor(sf: ts.SourceFile) { let hook = ""; const visitor: ts.Visitor = (node: ts.Node): ts.Node => { if (!hook) { hook = findMacroHook( node, sf, opts.macroImportName, opts.macroModuleSpecifier ); if (hook) { return null; } } // If it's not our macro, skip if (!isMacroExpression(node, sf, hook)) { return ts.visitEachChild(node, visitor, ctx); } const msgObjLiteral = node.arguments[0] as ts.ObjectLiteralExpression; const msg = extractMessageDescriptor( msgObjLiteral, sf, opts.interpolateName, opts.baseUrl ); if (typeof opts.onMsgExtracted === "function") { opts.onMsgExtracted(msg.id, msg, sf.fileName); } return ts.createObjectLiteral([ ts.createPropertyAssignment("id", ts.createStringLiteral(msg.id)), ts.createPropertyAssignment( "defaultMessage", ts.createStringLiteral(msg.defaultMessage) ) ]); }; return visitor; } return (sf: ts.SourceFile) => ts.visitNode(sf, getVisitor(sf)); }; }