2021-08-28 15:51:32 +03:00
|
|
|
|
// babel-plugin-react-translate
|
|
|
|
|
// (c) Vitaliy Filippov 2021+
|
|
|
|
|
// SPDX-License-Identifier: LGPL-3.0
|
|
|
|
|
|
2021-08-28 01:27:40 +03:00
|
|
|
|
const fs = require('fs');
|
|
|
|
|
|
|
|
|
|
module.exports = function(babel)
|
|
|
|
|
{
|
|
|
|
|
const t = babel.types;
|
|
|
|
|
const importAdded = new WeakSet();
|
|
|
|
|
const arg0 = new WeakSet();
|
2021-08-28 15:06:48 +03:00
|
|
|
|
const regexp = new WeakMap();
|
2021-08-28 01:27:40 +03:00
|
|
|
|
const ru = /[А-ЯЁа-яё]/;
|
|
|
|
|
const strings = {};
|
2021-08-28 15:06:48 +03:00
|
|
|
|
const getRegexp = function(state)
|
|
|
|
|
{
|
|
|
|
|
let re = regexp.get(state.opts);
|
|
|
|
|
if (!re)
|
|
|
|
|
{
|
|
|
|
|
re = new RegExp(state.opts.regexp || '[А-ЯЁа-яё]');
|
|
|
|
|
regexp.set(state.opts, re);
|
|
|
|
|
}
|
|
|
|
|
return re;
|
|
|
|
|
};
|
|
|
|
|
const splitWhite = function(str)
|
|
|
|
|
{
|
|
|
|
|
let l = /^\s+/.exec(str), r = /\s+$/.exec(str);
|
|
|
|
|
l = l ? l[0] : '';
|
|
|
|
|
r = r ? r[0] : '';
|
|
|
|
|
return [ l, str.substr(l.length, str.length-r.length-l.length), r ];
|
|
|
|
|
};
|
|
|
|
|
const withWhite = function(l, repl, r)
|
|
|
|
|
{
|
|
|
|
|
if (l)
|
|
|
|
|
repl = t.binaryExpression('+', t.stringLiteral(l), repl);
|
|
|
|
|
if (r)
|
|
|
|
|
repl = t.binaryExpression('+', repl, t.stringLiteral(r));
|
|
|
|
|
return repl;
|
|
|
|
|
};
|
2021-08-28 01:27:40 +03:00
|
|
|
|
const addString = function(path, str)
|
|
|
|
|
{
|
2021-08-28 16:35:36 +03:00
|
|
|
|
const fn = path.hub.file.opts.filename.substr(path.hub.file.opts.root.length+1);
|
|
|
|
|
strings[fn][str] = true;
|
2021-08-28 01:27:40 +03:00
|
|
|
|
};
|
|
|
|
|
const addImport = function(path)
|
|
|
|
|
{
|
|
|
|
|
const program = path.findParent(p => t.isProgram(p));
|
|
|
|
|
if (!program)
|
|
|
|
|
{
|
|
|
|
|
throw new Error('<Program> AST element not found, can\'t add import');
|
|
|
|
|
}
|
|
|
|
|
if (!importAdded.has(program))
|
|
|
|
|
{
|
|
|
|
|
program.unshiftContainer('body', t.importDeclaration(
|
|
|
|
|
[ t.importSpecifier(t.identifier('L'), t.identifier('L')) ],
|
|
|
|
|
t.stringLiteral('babel-plugin-react-translate/runtime')
|
|
|
|
|
));
|
|
|
|
|
importAdded.add(program);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
return {
|
|
|
|
|
visitor: {
|
|
|
|
|
Program: {
|
|
|
|
|
enter(path, state)
|
|
|
|
|
{
|
2021-08-28 16:35:36 +03:00
|
|
|
|
const fn = path.hub.file.opts.filename.substr(path.hub.file.opts.root.length+1);
|
|
|
|
|
strings[fn] = {};
|
2021-08-28 01:27:40 +03:00
|
|
|
|
},
|
|
|
|
|
exit(path, state)
|
|
|
|
|
{
|
2021-08-28 16:35:36 +03:00
|
|
|
|
const fn = path.hub.file.opts.filename.substr(path.hub.file.opts.root.length+1);
|
2021-08-28 01:27:40 +03:00
|
|
|
|
let found = false;
|
2021-08-28 16:35:36 +03:00
|
|
|
|
for (let k in strings[fn])
|
2021-08-28 01:27:40 +03:00
|
|
|
|
{
|
|
|
|
|
found = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (!found)
|
|
|
|
|
delete strings[path.hub.file.opts.filename];
|
2021-08-28 15:06:48 +03:00
|
|
|
|
const arrays = { ...strings };
|
|
|
|
|
for (let k in arrays)
|
2021-08-28 17:37:20 +03:00
|
|
|
|
{
|
2021-08-28 15:06:48 +03:00
|
|
|
|
arrays[k] = Object.keys(arrays[k]);
|
2021-08-28 17:37:20 +03:00
|
|
|
|
if (!arrays[k].length)
|
|
|
|
|
delete arrays[k];
|
|
|
|
|
}
|
2021-08-28 01:27:40 +03:00
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
path.hub.file.opts.root+'/'+(state.opts['output'] || 'react-translate-output.json'),
|
2021-08-28 15:06:48 +03:00
|
|
|
|
JSON.stringify(arrays, null, 2)
|
2021-08-28 01:27:40 +03:00
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
},
|
2021-08-28 15:06:48 +03:00
|
|
|
|
// Convert concatenated string literals and expressions to localization calls with arguments
|
|
|
|
|
// I.e. 'Really delete '+item.name+'?' -> L('Really delete {1}?', item.name)
|
|
|
|
|
BinaryExpression(path, state)
|
2021-08-28 01:27:40 +03:00
|
|
|
|
{
|
2021-08-28 15:06:48 +03:00
|
|
|
|
if (path.node.operator === '+')
|
|
|
|
|
{
|
|
|
|
|
let added = [ path.node.left, path.node.right ];
|
|
|
|
|
for (let i = 0; i < added.length; i++)
|
|
|
|
|
{
|
|
|
|
|
if (t.isBinaryExpression(added[i]) && added[i].operator === '+')
|
|
|
|
|
{
|
|
|
|
|
added.splice(i, 1, added[i].left, added[i].right);
|
|
|
|
|
i--;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let ru = getRegexp(state);
|
|
|
|
|
let i18n = false;
|
|
|
|
|
for (let i = 0; i < added.length; i++)
|
|
|
|
|
{
|
|
|
|
|
if (t.isStringLiteral(added[i]) && ru.exec(added[i].value))
|
|
|
|
|
{
|
|
|
|
|
i18n = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (i18n)
|
|
|
|
|
{
|
|
|
|
|
let tpl = '', expressions = [];
|
|
|
|
|
let lit = true, i = 0;
|
|
|
|
|
while (i < added.length)
|
|
|
|
|
{
|
|
|
|
|
if (lit)
|
|
|
|
|
{
|
|
|
|
|
let text = '';
|
|
|
|
|
while (i < added.length && t.isStringLiteral(added[i]))
|
|
|
|
|
text += added[i++].value;
|
|
|
|
|
tpl += text;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
let expr = null;
|
|
|
|
|
while (i < added.length && !t.isStringLiteral(added[i]))
|
|
|
|
|
expr = expr ? t.binaryExpression('+', expr, added[i++]) : added[i++];
|
|
|
|
|
expressions.push(expr);
|
|
|
|
|
tpl += '{'+expressions.length+'}';
|
|
|
|
|
}
|
|
|
|
|
lit = !lit;
|
|
|
|
|
}
|
|
|
|
|
let [ lwhite, text, rwhite ] = splitWhite(tpl);
|
|
|
|
|
addString(path, text);
|
|
|
|
|
text = t.stringLiteral(text);
|
|
|
|
|
// Stop the string literal from being visited again
|
|
|
|
|
arg0.add(text);
|
|
|
|
|
let repl = t.callExpression(t.identifier('L'), [ text, ...expressions ]);
|
|
|
|
|
repl = withWhite(lwhite, repl, rwhite);
|
|
|
|
|
path.replaceWith(repl);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// Convert simple string literals to localization calls
|
|
|
|
|
// I.e. "Hello" -> L("Hello")
|
|
|
|
|
Literal(path, state)
|
|
|
|
|
{
|
|
|
|
|
let ru = getRegexp(state);
|
2021-08-28 01:27:40 +03:00
|
|
|
|
if (ru.exec(path.node.value))
|
|
|
|
|
{
|
|
|
|
|
const parent = path.findParent(() => true);
|
2021-08-28 15:06:48 +03:00
|
|
|
|
const isJSX = parent.isJSXAttribute();
|
|
|
|
|
if (isJSX || !arg0.has(path.node) && !path.findParent(parent => arg0.has(parent.node)))
|
2021-08-28 01:27:40 +03:00
|
|
|
|
{
|
2021-08-28 15:06:48 +03:00
|
|
|
|
const [ lwhite, text, rwhite ] = splitWhite(path.node.value);
|
|
|
|
|
addString(path, text);
|
|
|
|
|
addImport(path);
|
|
|
|
|
let repl = t.callExpression(t.identifier('L'), [ t.stringLiteral(text) ]);
|
|
|
|
|
repl = withWhite(lwhite, repl, rwhite);
|
|
|
|
|
if (isJSX)
|
|
|
|
|
path.replaceWith(t.jsxExpressionContainer(repl));
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Stop the original string from being visited again
|
|
|
|
|
arg0.add(repl);
|
|
|
|
|
path.replaceWith(repl);
|
|
|
|
|
}
|
2021-08-28 01:27:40 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
CallExpression(path)
|
|
|
|
|
{
|
|
|
|
|
if (path.node.callee.type === 'Identifier' && path.node.callee.name === 'L')
|
|
|
|
|
{
|
2021-08-28 16:07:16 +03:00
|
|
|
|
if (t.isStringLiteral(path.node.arguments[0]))
|
|
|
|
|
{
|
|
|
|
|
// Remember the user-provided string
|
|
|
|
|
addString(path, path.node.arguments[0].value);
|
|
|
|
|
addImport(path);
|
|
|
|
|
}
|
2021-08-28 01:27:40 +03:00
|
|
|
|
// Skip the first argument
|
|
|
|
|
arg0.add(path.node.arguments[0]);
|
|
|
|
|
}
|
|
|
|
|
},
|
2021-08-28 15:06:48 +03:00
|
|
|
|
// Convert simple JSX literals to localization calls
|
|
|
|
|
// I.e. <b>Text</b> -> <b>{L("Text")}</b>
|
|
|
|
|
JSXText(path, state)
|
2021-08-28 01:27:40 +03:00
|
|
|
|
{
|
2021-08-28 15:06:48 +03:00
|
|
|
|
let ru = getRegexp(state);
|
2021-08-28 01:27:40 +03:00
|
|
|
|
if (ru.exec(path.node.value))
|
|
|
|
|
{
|
2021-10-12 22:47:59 +03:00
|
|
|
|
let [ lwhite, text, rwhite ] = splitWhite(path.node.value);
|
|
|
|
|
text = text.replace(/\s{2,}|[\n\r\t]/g, ' ');
|
2021-08-28 01:27:40 +03:00
|
|
|
|
addImport(path);
|
|
|
|
|
addString(path, text);
|
|
|
|
|
const repl = [];
|
|
|
|
|
if (lwhite)
|
2021-08-28 15:06:48 +03:00
|
|
|
|
repl.push(t.jsxText(lwhite));
|
2021-08-28 01:27:40 +03:00
|
|
|
|
repl.push(t.jsxExpressionContainer(t.callExpression(t.identifier('L'), [ t.stringLiteral(text) ])));
|
|
|
|
|
if (rwhite)
|
2021-08-28 15:06:48 +03:00
|
|
|
|
repl.push(t.jsxText(rwhite));
|
2021-08-28 01:27:40 +03:00
|
|
|
|
path.replaceWithMultiple(repl);
|
|
|
|
|
}
|
|
|
|
|
},
|
2021-08-28 15:06:48 +03:00
|
|
|
|
// Convert template literals to localization calls with arguments
|
|
|
|
|
// I.e. `Really delete ${item.name}?` -> L("Really delete {1}?", item.name)
|
|
|
|
|
TemplateLiteral(path, state)
|
2021-08-28 01:27:40 +03:00
|
|
|
|
{
|
2021-08-28 15:06:48 +03:00
|
|
|
|
let ru = getRegexp(state);
|
2021-08-28 01:27:40 +03:00
|
|
|
|
if (path.node.quasis.find(q => ru.exec(q.value.cooked)))
|
|
|
|
|
{
|
|
|
|
|
addImport(path);
|
2021-08-28 15:06:48 +03:00
|
|
|
|
let tpl = path.node.quasis[0].value.cooked;
|
2021-08-28 01:27:40 +03:00
|
|
|
|
for (let i = 1; i < path.node.quasis.length; i++)
|
2021-08-28 15:06:48 +03:00
|
|
|
|
tpl += '{'+i+'}'+path.node.quasis[i].value.cooked;
|
|
|
|
|
let [ lwhite, text, rwhite ] = splitWhite(tpl);
|
2021-08-28 01:27:40 +03:00
|
|
|
|
addString(path, text);
|
|
|
|
|
text = t.stringLiteral(text);
|
2021-08-28 15:06:48 +03:00
|
|
|
|
// Stop the string literal from being visited again
|
2021-08-28 01:27:40 +03:00
|
|
|
|
arg0.add(text);
|
2021-08-28 15:06:48 +03:00
|
|
|
|
let repl = t.callExpression(t.identifier('L'), [ text, ...path.node.expressions ]);
|
|
|
|
|
repl = withWhite(lwhite, repl, rwhite);
|
|
|
|
|
path.replaceWith(repl);
|
2021-08-28 01:27:40 +03:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|