refactor(localize): add placeholder locations in extracted messages (#38536)
Some translation file formats would like to be able to render the text of placeholders taken from the original source files. This commit adds this information to the extracted messages so that it can be used in translation file serializers. PR Close #38536
This commit is contained in:
parent
b8351f3b10
commit
db3a21b382
|
@ -9,7 +9,7 @@ import {ɵParsedMessage, ɵparseMessage} from '@angular/localize';
|
|||
import {NodePath, PluginObj} from '@babel/core';
|
||||
import {TaggedTemplateExpression} from '@babel/types';
|
||||
|
||||
import {getLocation, isGlobalIdentifier, isNamedIdentifier, unwrapMessagePartsFromTemplateLiteral} from '../../source_file_utils';
|
||||
import {getLocation, isGlobalIdentifier, isNamedIdentifier, unwrapExpressionsFromTemplateLiteral, unwrapMessagePartsFromTemplateLiteral} from '../../source_file_utils';
|
||||
|
||||
export function makeEs2015ExtractPlugin(
|
||||
messages: ɵParsedMessage[], localizeName = '$localize'): PluginObj {
|
||||
|
@ -18,9 +18,14 @@ export function makeEs2015ExtractPlugin(
|
|||
TaggedTemplateExpression(path: NodePath<TaggedTemplateExpression>) {
|
||||
const tag = path.get('tag');
|
||||
if (isNamedIdentifier(tag, localizeName) && isGlobalIdentifier(tag)) {
|
||||
const messageParts = unwrapMessagePartsFromTemplateLiteral(path.node.quasi.quasis);
|
||||
const location = getLocation(path.get('quasi'));
|
||||
const message = ɵparseMessage(messageParts, path.node.quasi.expressions, location);
|
||||
const quasiPath = path.get('quasi');
|
||||
const [messageParts, messagePartLocations] =
|
||||
unwrapMessagePartsFromTemplateLiteral(quasiPath.get('quasis'));
|
||||
const [expressions, expressionLocations] =
|
||||
unwrapExpressionsFromTemplateLiteral(quasiPath);
|
||||
const location = getLocation(quasiPath);
|
||||
const message = ɵparseMessage(
|
||||
messageParts, expressions, location, messagePartLocations, expressionLocations);
|
||||
messages.push(message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,12 @@ export function makeEs5ExtractPlugin(
|
|||
CallExpression(callPath: NodePath<CallExpression>) {
|
||||
const calleePath = callPath.get('callee');
|
||||
if (isNamedIdentifier(calleePath, localizeName) && isGlobalIdentifier(calleePath)) {
|
||||
const messageParts = unwrapMessagePartsFromLocalizeCall(callPath);
|
||||
const expressions = unwrapSubstitutionsFromLocalizeCall(callPath.node);
|
||||
const location = getLocation(callPath);
|
||||
const message = ɵparseMessage(messageParts, expressions, location);
|
||||
const [messageParts, messagePartLocations] = unwrapMessagePartsFromLocalizeCall(callPath);
|
||||
const [expressions, expressionLocations] = unwrapSubstitutionsFromLocalizeCall(callPath);
|
||||
const [messagePartsArg, expressionsArg] = callPath.get('arguments');
|
||||
const location = getLocation(messagePartsArg, expressionsArg);
|
||||
const message = ɵparseMessage(
|
||||
messageParts, expressions, location, messagePartLocations, expressionLocations);
|
||||
messages.push(message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ export function buildLocalizeReplacement(
|
|||
* @param call The AST node of the call to process.
|
||||
*/
|
||||
export function unwrapMessagePartsFromLocalizeCall(call: NodePath<t.CallExpression>):
|
||||
TemplateStringsArray {
|
||||
[TemplateStringsArray, (ɵSourceLocation | undefined)[]] {
|
||||
let cooked = call.get('arguments')[0];
|
||||
|
||||
if (cooked === undefined) {
|
||||
|
@ -137,34 +137,44 @@ export function unwrapMessagePartsFromLocalizeCall(call: NodePath<t.CallExpressi
|
|||
raw = arg2 !== undefined ? arg2 : cooked;
|
||||
}
|
||||
|
||||
const cookedStrings = unwrapStringLiteralArray(cooked.node);
|
||||
const rawStrings = unwrapStringLiteralArray(raw.node);
|
||||
return ɵmakeTemplateObject(cookedStrings, rawStrings);
|
||||
const [cookedStrings] = unwrapStringLiteralArray(cooked);
|
||||
const [rawStrings, rawLocations] = unwrapStringLiteralArray(raw);
|
||||
return [ɵmakeTemplateObject(cookedStrings, rawStrings), rawLocations];
|
||||
}
|
||||
|
||||
|
||||
export function unwrapSubstitutionsFromLocalizeCall(call: t.CallExpression): t.Expression[] {
|
||||
const expressions = call.arguments.splice(1);
|
||||
export function unwrapSubstitutionsFromLocalizeCall(call: NodePath<t.CallExpression>):
|
||||
[t.Expression[], (ɵSourceLocation | undefined)[]] {
|
||||
const expressions = call.get('arguments').splice(1);
|
||||
if (!isArrayOfExpressions(expressions)) {
|
||||
const badExpression = expressions.find(expression => !t.isExpression(expression))!;
|
||||
const badExpression = expressions.find(expression => !expression.isExpression())!;
|
||||
throw new BabelParseError(
|
||||
badExpression,
|
||||
badExpression.node,
|
||||
'Invalid substitutions for `$localize` (expected all substitution arguments to be expressions).');
|
||||
}
|
||||
return expressions;
|
||||
return [
|
||||
expressions.map(path => path.node), expressions.map(expression => getLocation(expression))
|
||||
];
|
||||
}
|
||||
|
||||
export function unwrapMessagePartsFromTemplateLiteral(elements: t.TemplateElement[]):
|
||||
TemplateStringsArray {
|
||||
export function unwrapMessagePartsFromTemplateLiteral(elements: NodePath<t.TemplateElement>[]):
|
||||
[TemplateStringsArray, (ɵSourceLocation | undefined)[]] {
|
||||
const cooked = elements.map(q => {
|
||||
if (q.value.cooked === undefined) {
|
||||
if (q.node.value.cooked === undefined) {
|
||||
throw new BabelParseError(
|
||||
q, `Unexpected undefined message part in "${elements.map(q => q.value.cooked)}"`);
|
||||
q.node,
|
||||
`Unexpected undefined message part in "${elements.map(q => q.node.value.cooked)}"`);
|
||||
}
|
||||
return q.value.cooked;
|
||||
return q.node.value.cooked;
|
||||
});
|
||||
const raw = elements.map(q => q.value.raw);
|
||||
return ɵmakeTemplateObject(cooked, raw);
|
||||
const raw = elements.map(q => q.node.value.raw);
|
||||
const locations = elements.map(q => getLocation(q));
|
||||
return [ɵmakeTemplateObject(cooked, raw), locations];
|
||||
}
|
||||
|
||||
export function unwrapExpressionsFromTemplateLiteral(quasi: NodePath<t.TemplateLiteral>):
|
||||
[t.Expression[], (ɵSourceLocation | undefined)[]] {
|
||||
return [quasi.node.expressions, quasi.get('expressions').map(e => getLocation(e))];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -186,12 +196,14 @@ export function wrapInParensIfNecessary(expression: t.Expression): t.Expression
|
|||
* Extract the string values from an `array` of string literals.
|
||||
* @param array The array to unwrap.
|
||||
*/
|
||||
export function unwrapStringLiteralArray(array: t.Expression): string[] {
|
||||
if (!isStringLiteralArray(array)) {
|
||||
export function unwrapStringLiteralArray(array: NodePath<t.Expression>):
|
||||
[string[], (ɵSourceLocation | undefined)[]] {
|
||||
if (!isStringLiteralArray(array.node)) {
|
||||
throw new BabelParseError(
|
||||
array, 'Unexpected messageParts for `$localize` (expected an array of strings).');
|
||||
array.node, 'Unexpected messageParts for `$localize` (expected an array of strings).');
|
||||
}
|
||||
return array.elements.map((str: t.StringLiteral) => str.value);
|
||||
const elements = array.get('elements') as NodePath<t.StringLiteral>[];
|
||||
return [elements.map(str => str.node.value), elements.map(str => getLocation(str))];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -295,8 +307,8 @@ export function isStringLiteralArray(node: t.Node): node is t.Expression&
|
|||
* Are all the given `nodes` expressions?
|
||||
* @param nodes The nodes to test.
|
||||
*/
|
||||
export function isArrayOfExpressions(nodes: t.Node[]): nodes is t.Expression[] {
|
||||
return nodes.every(element => t.isExpression(element));
|
||||
export function isArrayOfExpressions(paths: NodePath<t.Node>[]): paths is NodePath<t.Expression>[] {
|
||||
return paths.every(element => element.isExpression());
|
||||
}
|
||||
|
||||
/** Options that affect how the `makeEsXXXTranslatePlugin()` functions work. */
|
||||
|
@ -361,7 +373,8 @@ export function getLocation(startPath: NodePath, endPath?: NodePath): ɵSourceLo
|
|||
return {
|
||||
start: getLineAndColumn(startLocation.start),
|
||||
end: getLineAndColumn(endLocation.end),
|
||||
file
|
||||
file,
|
||||
text: getText(startPath),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -375,7 +388,7 @@ export function serializeLocationPosition(location: ɵSourceLocation): string {
|
|||
function getFileFromPath(path: NodePath|undefined): AbsoluteFsPath|null {
|
||||
const opts = path?.hub.file.opts;
|
||||
return opts?.filename ?
|
||||
resolve(opts.generatorOpts.sourceRoot, relative(opts.cwd, opts.filename)) :
|
||||
resolve(opts.generatorOpts.sourceRoot ?? opts.cwd, relative(opts.cwd, opts.filename)) :
|
||||
null;
|
||||
}
|
||||
|
||||
|
@ -383,3 +396,10 @@ function getLineAndColumn(loc: {line: number, column: number}): {line: number, c
|
|||
// Note we want 0-based line numbers but Babel returns 1-based.
|
||||
return {line: loc.line - 1, column: loc.column};
|
||||
}
|
||||
|
||||
function getText(path: NodePath): string|undefined {
|
||||
if (path.node.start === null || path.node.end === null) {
|
||||
return undefined;
|
||||
}
|
||||
return path.hub.file.code.substring(path.node.start, path.node.end);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,8 @@ export function makeEs2015TranslatePlugin(
|
|||
try {
|
||||
const tag = path.get('tag');
|
||||
if (isLocalize(tag, localizeName)) {
|
||||
const messageParts = unwrapMessagePartsFromTemplateLiteral(path.node.quasi.quasis);
|
||||
const [messageParts] =
|
||||
unwrapMessagePartsFromTemplateLiteral(path.get('quasi').get('quasis'));
|
||||
const translated = translate(
|
||||
diagnostics, translations, messageParts, path.node.quasi.expressions,
|
||||
missingTranslation);
|
||||
|
|
|
@ -23,8 +23,8 @@ export function makeEs5TranslatePlugin(
|
|||
try {
|
||||
const calleePath = callPath.get('callee');
|
||||
if (isLocalize(calleePath, localizeName)) {
|
||||
const messageParts = unwrapMessagePartsFromLocalizeCall(callPath);
|
||||
const expressions = unwrapSubstitutionsFromLocalizeCall(callPath.node);
|
||||
const [messageParts] = unwrapMessagePartsFromLocalizeCall(callPath);
|
||||
const [expressions] = unwrapSubstitutionsFromLocalizeCall(callPath);
|
||||
const translated =
|
||||
translate(diagnostics, translations, messageParts, expressions, missingTranslation);
|
||||
callPath.replaceWith(buildLocalizeReplacement(translated[0], translated[1]));
|
||||
|
|
|
@ -36,11 +36,50 @@ runInEachFileSystem(() => {
|
|||
description: 'description',
|
||||
meaning: 'meaning',
|
||||
messageParts: ['a', 'b', 'c'],
|
||||
messagePartLocations: [
|
||||
{
|
||||
start: {line: 0, column: 10},
|
||||
end: {line: 0, column: 32},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: ':meaning|description:a',
|
||||
},
|
||||
{
|
||||
start: {line: 0, column: 36},
|
||||
end: {line: 0, column: 37},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: 'b',
|
||||
},
|
||||
{
|
||||
start: {line: 0, column: 41},
|
||||
end: {line: 0, column: 42},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: 'c',
|
||||
}
|
||||
],
|
||||
text: 'a{$PH}b{$PH_1}c',
|
||||
placeholderNames: ['PH', 'PH_1'],
|
||||
substitutions: jasmine.any(Object),
|
||||
substitutionLocations: {
|
||||
PH: {
|
||||
start: {line: 0, column: 34},
|
||||
end: {line: 0, column: 35},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: '1'
|
||||
},
|
||||
PH_1: {
|
||||
start: {line: 0, column: 39},
|
||||
end: {line: 0, column: 40},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: '2'
|
||||
}
|
||||
},
|
||||
legacyIds: [],
|
||||
location: {start: {line: 0, column: 9}, end: {line: 0, column: 43}, file},
|
||||
location: {
|
||||
start: {line: 0, column: 9},
|
||||
end: {line: 0, column: 43},
|
||||
file,
|
||||
text: '`:meaning|description:a${1}b${2}c`',
|
||||
},
|
||||
});
|
||||
|
||||
expect(messages[1]).toEqual({
|
||||
|
@ -49,11 +88,51 @@ runInEachFileSystem(() => {
|
|||
description: '',
|
||||
meaning: '',
|
||||
messageParts: ['a', 'b', 'c'],
|
||||
messagePartLocations: [
|
||||
{
|
||||
start: {line: 1, column: 69},
|
||||
end: {line: 1, column: 72},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: '"a"',
|
||||
},
|
||||
{
|
||||
start: {line: 1, column: 74},
|
||||
end: {line: 1, column: 97},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: '":custom-placeholder:b"',
|
||||
},
|
||||
{
|
||||
start: {line: 1, column: 99},
|
||||
end: {line: 1, column: 102},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: '"c"',
|
||||
}
|
||||
],
|
||||
text: 'a{$custom-placeholder}b{$PH_1}c',
|
||||
placeholderNames: ['custom-placeholder', 'PH_1'],
|
||||
substitutions: jasmine.any(Object),
|
||||
substitutionLocations: {
|
||||
'custom-placeholder': {
|
||||
start: {line: 1, column: 106},
|
||||
end: {line: 1, column: 107},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: '1'
|
||||
},
|
||||
PH_1: {
|
||||
start: {line: 1, column: 109},
|
||||
end: {line: 1, column: 110},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: '2'
|
||||
}
|
||||
},
|
||||
legacyIds: [],
|
||||
location: {start: {line: 1, column: 0}, end: {line: 1, column: 111}, file},
|
||||
location: {
|
||||
start: {line: 1, column: 10},
|
||||
end: {line: 1, column: 107},
|
||||
file,
|
||||
text:
|
||||
'__makeTemplateObject(["a", ":custom-placeholder:b", "c"], ["a", ":custom-placeholder:b", "c"])',
|
||||
},
|
||||
});
|
||||
|
||||
expect(messages[2]).toEqual({
|
||||
|
@ -65,8 +144,47 @@ runInEachFileSystem(() => {
|
|||
text: 'a{$PH}b{$PH_1}c',
|
||||
placeholderNames: ['PH', 'PH_1'],
|
||||
substitutions: jasmine.any(Object),
|
||||
substitutionLocations: {
|
||||
PH: {
|
||||
start: {line: 2, column: 26},
|
||||
end: {line: 2, column: 27},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: '1'
|
||||
},
|
||||
PH_1: {
|
||||
start: {line: 2, column: 31},
|
||||
end: {line: 2, column: 32},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: '2'
|
||||
}
|
||||
},
|
||||
messagePartLocations: [
|
||||
{
|
||||
start: {line: 2, column: 10},
|
||||
end: {line: 2, column: 24},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: ':@@custom-id:a'
|
||||
},
|
||||
{
|
||||
start: {line: 2, column: 28},
|
||||
end: {line: 2, column: 29},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: 'b'
|
||||
},
|
||||
{
|
||||
start: {line: 2, column: 33},
|
||||
end: {line: 2, column: 34},
|
||||
file: absoluteFrom('/root/path/relative/path.js'),
|
||||
text: 'c'
|
||||
}
|
||||
],
|
||||
legacyIds: [],
|
||||
location: {start: {line: 2, column: 9}, end: {line: 2, column: 35}, file},
|
||||
location: {
|
||||
start: {line: 2, column: 9},
|
||||
end: {line: 2, column: 35},
|
||||
file,
|
||||
text: '`:@@custom-id:a${1}b${2}c`'
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,10 +12,11 @@ import {NodePath, TransformOptions, transformSync} from '@babel/core';
|
|||
import generate from '@babel/generator';
|
||||
|
||||
import template from '@babel/template';
|
||||
import {Expression, Identifier, TaggedTemplateExpression, ExpressionStatement, FunctionDeclaration, CallExpression, isParenthesizedExpression, numericLiteral, binaryExpression, NumericLiteral} from '@babel/types';
|
||||
import {Expression, Identifier, TaggedTemplateExpression, ExpressionStatement, CallExpression, isParenthesizedExpression, numericLiteral, binaryExpression, NumericLiteral} from '@babel/types';
|
||||
import {isGlobalIdentifier, isNamedIdentifier, isStringLiteralArray, isArrayOfExpressions, unwrapStringLiteralArray, unwrapMessagePartsFromLocalizeCall, wrapInParensIfNecessary, buildLocalizeReplacement, unwrapSubstitutionsFromLocalizeCall, unwrapMessagePartsFromTemplateLiteral, getLocation} from '../src/source_file_utils';
|
||||
|
||||
describe('utils', () => {
|
||||
runInEachFileSystem(() => {
|
||||
describe('utils', () => {
|
||||
describe('isNamedIdentifier()', () => {
|
||||
it('should return true if the expression is an identifier with name `$localize`', () => {
|
||||
const taggedTemplate = getTaggedTemplate('$localize ``;');
|
||||
|
@ -73,30 +74,93 @@ describe('utils', () => {
|
|||
});
|
||||
|
||||
describe('unwrapMessagePartsFromLocalizeCall', () => {
|
||||
it('should return an array of string literals from a direct call to a tag function', () => {
|
||||
it('should return an array of string literals and locations from a direct call to a tag function',
|
||||
() => {
|
||||
const localizeCall = getLocalizeCall(`$localize(['a', 'b\\t', 'c'], 1, 2)`);
|
||||
const parts = unwrapMessagePartsFromLocalizeCall(localizeCall);
|
||||
const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall);
|
||||
expect(parts).toEqual(['a', 'b\t', 'c']);
|
||||
expect(locations).toEqual([
|
||||
{
|
||||
start: {line: 0, column: 11},
|
||||
end: {line: 0, column: 14},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: `'a'`,
|
||||
},
|
||||
{
|
||||
start: {line: 0, column: 16},
|
||||
end: {line: 0, column: 21},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: `'b\\t'`,
|
||||
},
|
||||
{
|
||||
start: {line: 0, column: 23},
|
||||
end: {line: 0, column: 26},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: `'c'`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an array of string literals from a downleveled tagged template', () => {
|
||||
it('should return an array of string literals and locations from a downleveled tagged template',
|
||||
() => {
|
||||
let localizeCall = getLocalizeCall(
|
||||
`$localize(__makeTemplateObject(['a', 'b\\t', 'c'], ['a', 'b\\\\t', 'c']), 1, 2)`);
|
||||
const parts = unwrapMessagePartsFromLocalizeCall(localizeCall);
|
||||
const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall);
|
||||
expect(parts).toEqual(['a', 'b\t', 'c']);
|
||||
expect(parts.raw).toEqual(['a', 'b\\t', 'c']);
|
||||
expect(locations).toEqual([
|
||||
{
|
||||
start: {line: 0, column: 51},
|
||||
end: {line: 0, column: 54},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: `'a'`,
|
||||
},
|
||||
{
|
||||
start: {line: 0, column: 56},
|
||||
end: {line: 0, column: 62},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: `'b\\\\t'`,
|
||||
},
|
||||
{
|
||||
start: {line: 0, column: 64},
|
||||
end: {line: 0, column: 67},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: `'c'`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an array of string literals from a lazy load template helper', () => {
|
||||
it('should return an array of string literals and locations from a lazy load template helper',
|
||||
() => {
|
||||
let localizeCall = getLocalizeCall(`
|
||||
function _templateObject() {
|
||||
var e = _taggedTemplateLiteral(['a', 'b', 'c'], ['a', 'b', 'c']);
|
||||
var e = _taggedTemplateLiteral(['a', 'b\\t', 'c'], ['a', 'b\\\\t', 'c']);
|
||||
return _templateObject = function() { return e }, e
|
||||
}
|
||||
$localize(_templateObject(), 1, 2)`);
|
||||
const parts = unwrapMessagePartsFromLocalizeCall(localizeCall);
|
||||
expect(parts).toEqual(['a', 'b', 'c']);
|
||||
expect(parts.raw).toEqual(['a', 'b', 'c']);
|
||||
const [parts, locations] = unwrapMessagePartsFromLocalizeCall(localizeCall);
|
||||
expect(parts).toEqual(['a', 'b\t', 'c']);
|
||||
expect(parts.raw).toEqual(['a', 'b\\t', 'c']);
|
||||
expect(locations).toEqual([
|
||||
{
|
||||
start: {line: 2, column: 61},
|
||||
end: {line: 2, column: 64},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: `'a'`,
|
||||
},
|
||||
{
|
||||
start: {line: 2, column: 66},
|
||||
end: {line: 2, column: 72},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: `'b\\\\t'`,
|
||||
},
|
||||
{
|
||||
start: {line: 2, column: 74},
|
||||
end: {line: 2, column: 77},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: `'c'`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove a lazy load template helper', () => {
|
||||
|
@ -116,27 +180,53 @@ describe('utils', () => {
|
|||
});
|
||||
|
||||
describe('unwrapSubstitutionsFromLocalizeCall', () => {
|
||||
it('should return the substitutions from a direct call to a tag function', () => {
|
||||
const ast = template.ast`$localize(['a', 'b\t', 'c'], 1, 2)` as ExpressionStatement;
|
||||
const call = ast.expression as CallExpression;
|
||||
const substitutions = unwrapSubstitutionsFromLocalizeCall(call);
|
||||
expect(substitutions.map(s => (s as NumericLiteral).value)).toEqual([1, 2]);
|
||||
it('should return the substitutions and locations from a direct call to a tag function',
|
||||
() => {
|
||||
const call = getLocalizeCall(`$localize(['a', 'b\t', 'c'], 1, 2)`);
|
||||
const [substitutions, locations] = unwrapSubstitutionsFromLocalizeCall(call);
|
||||
expect((substitutions as NumericLiteral[]).map(s => s.value)).toEqual([1, 2]);
|
||||
expect(locations).toEqual([
|
||||
{
|
||||
start: {line: 0, column: 28},
|
||||
end: {line: 0, column: 29},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: '1'
|
||||
},
|
||||
{
|
||||
start: {line: 0, column: 31},
|
||||
end: {line: 0, column: 32},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: '2'
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the substitutions from a downleveled tagged template', () => {
|
||||
const ast = template.ast
|
||||
`$localize(__makeTemplateObject(['a', 'b', 'c'], ['a', 'b', 'c']), 1, 2)` as
|
||||
ExpressionStatement;
|
||||
const call = ast.expression as CallExpression;
|
||||
const substitutions = unwrapSubstitutionsFromLocalizeCall(call);
|
||||
expect(substitutions.map(s => (s as NumericLiteral).value)).toEqual([1, 2]);
|
||||
it('should return the substitutions and locations from a downleveled tagged template', () => {
|
||||
const call = getLocalizeCall(
|
||||
`$localize(__makeTemplateObject(['a', 'b', 'c'], ['a', 'b', 'c']), 1, 2)`);
|
||||
const [substitutions, locations] = unwrapSubstitutionsFromLocalizeCall(call);
|
||||
expect((substitutions as NumericLiteral[]).map(s => s.value)).toEqual([1, 2]);
|
||||
expect(locations).toEqual([
|
||||
{
|
||||
start: {line: 0, column: 66},
|
||||
end: {line: 0, column: 67},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: '1'
|
||||
},
|
||||
{
|
||||
start: {line: 0, column: 69},
|
||||
end: {line: 0, column: 70},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: '2'
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unwrapMessagePartsFromTemplateLiteral', () => {
|
||||
it('should return a TemplateStringsArray built from the template literal elements', () => {
|
||||
const taggedTemplate = getTaggedTemplate('$localize `a${1}b\\t${2}c`;');
|
||||
expect(unwrapMessagePartsFromTemplateLiteral(taggedTemplate.node.quasi.quasis))
|
||||
expect(unwrapMessagePartsFromTemplateLiteral(taggedTemplate.get('quasi').get('quasis'))[0])
|
||||
.toEqual(ɵmakeTemplateObject(['a', 'b\t', 'c'], ['a', 'b\\t', 'c']));
|
||||
});
|
||||
});
|
||||
|
@ -157,14 +247,36 @@ describe('utils', () => {
|
|||
|
||||
describe('unwrapStringLiteralArray', () => {
|
||||
it('should return an array of string from an array expression', () => {
|
||||
const ast = template.ast`['a', 'b', 'c']` as ExpressionStatement;
|
||||
expect(unwrapStringLiteralArray(ast.expression)).toEqual(['a', 'b', 'c']);
|
||||
const array = getFirstExpression(`['a', 'b', 'c']`);
|
||||
const [expressions, locations] = unwrapStringLiteralArray(array);
|
||||
expect(expressions).toEqual(['a', 'b', 'c']);
|
||||
expect(locations).toEqual([
|
||||
{
|
||||
start: {line: 0, column: 1},
|
||||
end: {line: 0, column: 4},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: `'a'`,
|
||||
},
|
||||
{
|
||||
start: {line: 0, column: 6},
|
||||
end: {line: 0, column: 9},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: `'b'`,
|
||||
},
|
||||
{
|
||||
start: {line: 0, column: 11},
|
||||
end: {line: 0, column: 14},
|
||||
file: absoluteFrom('/test/file.js'),
|
||||
text: `'c'`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error if any elements of the array are not literal strings', () => {
|
||||
const ast = template.ast`['a', 2, 'c']` as ExpressionStatement;
|
||||
expect(() => unwrapStringLiteralArray(ast.expression))
|
||||
.toThrowError('Unexpected messageParts for `$localize` (expected an array of strings).');
|
||||
const array = getFirstExpression(`['a', 2, 'c']`);
|
||||
expect(() => unwrapStringLiteralArray(array))
|
||||
.toThrowError(
|
||||
'Unexpected messageParts for `$localize` (expected an array of strings).');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -187,17 +299,16 @@ describe('utils', () => {
|
|||
|
||||
describe('isArrayOfExpressions()', () => {
|
||||
it('should return true if all the nodes are expressions', () => {
|
||||
const ast = template.ast`function foo(a, b, c) {}` as FunctionDeclaration;
|
||||
expect(isArrayOfExpressions(ast.params)).toBe(true);
|
||||
const call = getFirstExpression<CallExpression>('foo(a, b, c);');
|
||||
expect(isArrayOfExpressions(call.get('arguments'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if any of the nodes is not an expression', () => {
|
||||
const ast = template.ast`function foo(a, b, ...c) {}` as FunctionDeclaration;
|
||||
expect(isArrayOfExpressions(ast.params)).toBe(false);
|
||||
const call = getFirstExpression<CallExpression>('foo(a, b, ...c);');
|
||||
expect(isArrayOfExpressions(call.get('arguments'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('getLocation()', () => {
|
||||
it('should return a plain object containing the start, end and file of a NodePath', () => {
|
||||
const taggedTemplate = getTaggedTemplate('const x = $localize `message`;', {
|
||||
|
@ -214,7 +325,8 @@ describe('utils', () => {
|
|||
});
|
||||
|
||||
it('should return `undefined` if the NodePath has no filename', () => {
|
||||
const taggedTemplate = getTaggedTemplate('const x = $localize ``;', {sourceRoot: '/root'});
|
||||
const taggedTemplate = getTaggedTemplate(
|
||||
'const x = $localize ``;', {sourceRoot: '/root', filename: undefined});
|
||||
const location = getLocation(taggedTemplate);
|
||||
expect(location).toBeUndefined();
|
||||
});
|
||||
|
@ -224,24 +336,38 @@ describe('utils', () => {
|
|||
|
||||
function getTaggedTemplate(
|
||||
code: string, options?: TransformOptions): NodePath<TaggedTemplateExpression> {
|
||||
const {expressions, plugin} = collectExpressionsPlugin();
|
||||
transformSync(code, {...options, plugins: [plugin]});
|
||||
return expressions.find(e => e.isTaggedTemplateExpression()) as any;
|
||||
return getExpressions<TaggedTemplateExpression>(code, options)
|
||||
.find(e => e.isTaggedTemplateExpression())!;
|
||||
}
|
||||
|
||||
function collectExpressionsPlugin() {
|
||||
function getFirstExpression<T extends Expression>(
|
||||
code: string, options?: TransformOptions): NodePath<T> {
|
||||
return getExpressions<T>(code, options)[0];
|
||||
}
|
||||
|
||||
function getExpressions<T extends Expression>(
|
||||
code: string, options?: TransformOptions): NodePath<T>[] {
|
||||
const expressions: NodePath<Expression>[] = [];
|
||||
const visitor = {
|
||||
transformSync(code, {
|
||||
code: false,
|
||||
filename: '/test/file.js',
|
||||
plugins: [{
|
||||
visitor: {
|
||||
Expression: (path: NodePath<Expression>) => {
|
||||
expressions.push(path);
|
||||
}
|
||||
};
|
||||
return {expressions, plugin: {visitor}};
|
||||
}
|
||||
}],
|
||||
...options
|
||||
});
|
||||
return expressions as NodePath<T>[];
|
||||
}
|
||||
|
||||
function getLocalizeCall(code: string): NodePath<CallExpression> {
|
||||
let callPaths: NodePath<CallExpression>[] = [];
|
||||
transformSync(code, {
|
||||
code: false,
|
||||
filename: '/test/file.js',
|
||||
plugins: [{
|
||||
visitor: {
|
||||
CallExpression(path) {
|
||||
|
|
Loading…
Reference in New Issue