UNPKG

@ckeditor/ckeditor5-dev-utils

Version:
150 lines (121 loc) 3.88 kB
/** * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ 'use strict'; const acorn = require( 'acorn' ); const walk = require( 'acorn-walk' ); /** * Parses source and finds messages from the first argument of `t()` calls. * * @param {String} source A content of the JS file that will be translated. * @param {String} sourceFile A path to source file, used only for creating error messages. * @param {(msg: Message) => void} onMessageFound * @param {(err: string) => void} onErrorFound * @returns {String} Transformed source. */ module.exports = function findMessages( source, sourceFile, onMessageFound, onErrorFound ) { const ast = acorn.parse( source, { sourceType: 'module', ranges: true, ecmaVersion: 10 } ); walk.simple( ast, { CallExpression: node => { try { findMessagesInNode( node ); } catch ( err ) { onErrorFound( 'CKEditor5 Translation tool found problem. \n' + err.stack ); } } } ); function findMessagesInNode( node ) { // Log a warning for the `editor.t()` calls and similar which aren't supported yet. if ( isTMethodCallExpression( node ) ) { const objName = node.callee.object.name; const propName = node.callee.property.name; onErrorFound( // TODO - ${ objName }.${ propName } is naive. `Found '${ objName }.${ propName }()' in the ${ sourceFile }. ` + 'Only messages from direct \'t()\' calls will be handled by CKEditor 5 translation mechanisms.' ); } if ( !isTFunctionCallExpression( node ) ) { return; } const firstArgument = node.arguments[ 0 ]; findMessagesInExpression( firstArgument ); } function findMessagesInExpression( node ) { // Matches t( { string: 'foo' } ) and t( { 'string': 'foo' } ). // (also `plural` and `id` properties) if ( node.type === 'ObjectExpression' ) { const properties = node.properties || []; const idProperty = getProperty( properties, 'id' ); const stringProperty = getProperty( properties, 'string' ); const pluralProperty = getProperty( properties, 'plural' ); // TODO - value assertions. /** @type {Message} */ const message = { string: stringProperty.value.value, id: stringProperty.value.value }; if ( idProperty ) { message.id = idProperty.value.value; } if ( pluralProperty ) { message.plural = pluralProperty.value.value; } onMessageFound( message ); return; } // Matches t( 'foo' ) if ( node.type === 'Literal' ) { onMessageFound( { string: node.value, id: node.value } ); return; } // Matches t( foo ? 'bar' : { string: 'baz', plural: 'biz' } ); if ( node.type === 'ConditionalExpression' ) { findMessagesInExpression( node.consequent ); findMessagesInExpression( node.alternate ); return; } onErrorFound( `First t() call argument should be a string literal or an object literal (${ sourceFile }).` ); } }; // Get property from the list of properties // It supports both forms: `{ propertyName: foo }` and `{ 'propertyName': 'foo' }` function getProperty( properties, propertyName ) { return properties.find( property => { if ( property.key.type === 'Identifier' ) { return property.key.name === propertyName; } if ( property.key.type === 'Literal' ) { return property.key.value === propertyName; } } ); } function isTFunctionCallExpression( node ) { return node.callee.name === 't'; } function isTMethodCallExpression( node ) { return ( node.callee.type === 'MemberExpression' && node.callee.property.name === 't' && // Skip this.t() calls. // These calls can be used frequently in the minified/ obfuscated code. node.callee.object.type !== 'ThisExpression' ); } /** * @typedef {Object} Message * * @property {String} id * @property {String} string * @property {String} [plural] */