/** * Internal dependencies */ const { TRANSLATION_FUNCTIONS, REGEXP_SPRINTF_PLACEHOLDER, getTranslateFunctionName, getTranslateFunctionArgs, getTextContentFromNode, } = require( '../utils' ); module.exports = { meta: { type: 'problem', messages: { missing: 'Translation function with placeholders is missing preceding translator comment', }, }, create( context ) { return { CallExpression( node ) { const { callee, loc: { start: { line: currentLine }, }, parent, arguments: args, } = node; const functionName = getTranslateFunctionName( callee ); if ( ! TRANSLATION_FUNCTIONS.has( functionName ) ) { return; } const candidates = getTranslateFunctionArgs( functionName, args ).map( getTextContentFromNode ); if ( candidates.filter( Boolean ).length === 0 ) { return; } const hasPlaceholders = candidates.some( ( candidate ) => REGEXP_SPRINTF_PLACEHOLDER.test( candidate ) ); // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test#Using_test()_on_a_regex_with_the_global_flag. REGEXP_SPRINTF_PLACEHOLDER.lastIndex = 0; if ( ! hasPlaceholders ) { return; } const comments = context.getCommentsBefore( node ).slice(); let parentNode = parent; /** * Loop through all parent nodes and get their preceding comments as well. * * This way we can gather comments that are not directly preceding the translation * function call, but are just on the line above it. This case is commonly supported * by string extraction tools like WP-CLI's i18n command. */ while ( parentNode && parentNode.type !== 'Program' && Math.abs( parentNode.loc.start.line - currentLine ) <= 1 ) { comments.push( ...context.getCommentsBefore( parentNode ) ); parentNode = parentNode.parent; } for ( const comment of comments ) { const { value: commentText, loc: { end: { line: commentLine }, }, } = comment; /* Skip cases like this: // translators: %s: Preference console.log( sprintf( __( 'Preference: %s' ), preference ) ); */ if ( Math.abs( commentLine - currentLine ) > 1 ) { break; } if ( /translators:\s*\S+/i.test( commentText ) ) { return; } } context.report( { node, messageId: 'missing', } ); }, }; }, };