@ckeditor/ckeditor5-dev-utils
Version:
Utils for CKEditor 5 development tools packages.
150 lines (121 loc) • 3.88 kB
JavaScript
/**
* @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]
*/