vue-i18n-customized-extractor
Version:
A CLI tool to extract text and i18n keys from Vue.js files.
970 lines (913 loc) • 74.6 kB
JavaScript
// <div> Hi {{ name }}</div> -> "Hi { name }"
// <div> {{ `Hei ${name} literal`}}</div> -> "`Hei ${name} literal`"
// <div> KK{{$t("HiKKT { name }", {name: name})}} </div> -> if includeT: "HiKKT { name }"; if not includeT: skip it
// <div> {{$t("Hello")}} </div> -> if includeT: "Hello"; if not includeT: skip it
// <div> {{$t("Hi { name }", {name: name})}} </div> -> if includeT: "Hi { name }"; if not includeT: skip it
// <div> {{ name }}</div> -> skip it
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const { parse } = require('@vue/compiler-sfc');
const { compile } = require('@vue/compiler-dom');
const babelParser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const harcode_skip_string_collection = [
//value of 'typeof' in Vue.js
'string',
'object',
// common strings that don't need translation
'MM/DD/YYYY',
'YYYY-MM-DD',
];
function run(targetDir, options = {}) {
const includeT = options.includeT || false;
const excludeFiles = options.excludeFiles || [];
const pageKeys = options.pageKeys || [];
const configFilePath = path.resolve(process.cwd(), options.configFilePath);
const templateOnly = options.templateOnly;
// NOTES: If the config file is not provided, it will use the default config file path: vue-i18n-customized-extractor.config.cjs
// NOTES: If no config file is found, it will use the default config file data: null
var configFileData = null;
if(fs.existsSync(configFilePath)){
configFileData = require(configFilePath);
}
const customizedComponentsMapping = parseCustomizedComponentMapping(configFileData?.customizedComponentsTranslationMap, pageKeys);
const extractedStrings = new Set();
const extractedStringsOfTranslationAsWholePointer = new Set();
var vueFiles = [];
var jsFiles = [];
const stat = fs.statSync(targetDir);
// Step 1: Find files
if (stat.isFile()) {
console.log(`Processing single file: ${targetDir}`);
if (targetDir.endsWith('.vue')) {
vueFiles = [targetDir];
} else if (!templateOnly && (targetDir.endsWith('.js') || targetDir.endsWith('.ts'))) {
jsFiles = [targetDir];
} else {
console.warn(`⚠️ Skipping unsupported file: ${targetDir}`);
return; // Nothing to do
}
} else {
const ignorePatterns = [
'**/node_modules/**',
...excludeFiles.flatMap(f =>
fs.existsSync(f) && fs.statSync(f).isDirectory()
? [f, `${f}/**`] // ignore the folder and everything under it
: [f] // ignore the file
),
];
console.log(`Processing folder: ${targetDir}`);
vueFiles = glob.sync(`${targetDir}/**/*.vue`, { ignore: ignorePatterns });
// If templateOnly is true, we only process .vue files
if(!templateOnly){
jsFiles = glob.sync(`${targetDir}/**/*.{js,ts}`, { ignore: ignorePatterns });
}
}
console.log(`Found ${vueFiles.length} .vue files: ${vueFiles.join(', ')}`);
console.log(`Found ${jsFiles.length} .js/.ts files: ${jsFiles.join(', ')}`);
// Step 2: Process Vue files
vueFiles.forEach(file => processVueFile(file, extractedStrings, extractedStringsOfTranslationAsWholePointer, includeT, customizedComponentsMapping, templateOnly));
// Step 3: Process JS/TS files
jsFiles.forEach(file => processJSContent(file, extractedStrings, includeT, true));
// Step 4: Output JSON
const outputOfExtractedStrings = {};
Array.from(extractedStrings).sort().forEach(strObjStr => {
const strObj = JSON.parse(strObjStr);
const { result: str, hasTCall } = strObj;
const strTrimmed = str.trim();
// Check if the string is a bracket notation (e.g. TestPage['Hi {name}'])
// **NOTES: Since each page keeps its own translation key-value mapping,
// **NOTES: normally if we extracted exsiting $t() calls, we can assume it will be something like: $t("TestPage['Hi {name}']")
// **NOTES: In extractedStrings, we already extracted the string without the $t() call, so it will be like: "TestPage['Hi {name}']"
// **NOTES: If the string is in bracket notation, we will parse it to be: eg. TestPage: { "Hi {name}": "Hi {name}" }
const parsedResult = parseBracketNotation(strTrimmed);
if( includeT && hasTCall) {
if(!outputOfExtractedStrings["Existing $t() calls' keys"]){
// If the "Existing $t() calls' keys" does not exist, create a new object
outputOfExtractedStrings["Existing $t() calls' keys"] = {};
}
if (parsedResult) {
// If the string is in bracket notation, parse it to be:
// eg. TestPage: { "Hi {name}": "Hi {name}" }
const { objectName: pageKey, keyName:pageTranslateValue } = parsedResult;
// console.log(!isUrl(pageTranslateValue));
if(pageTranslateValue && !isUrl(pageTranslateValue) && !harcode_skip_string_collection.includes(pageTranslateValue)){
// NOTES: doesn't check isLikelyVariableLike(pageTranslateValue) here, because if parsedResult has value, most likely it is a key extracted from a $t() call when includeT is true
if(!outputOfExtractedStrings["Existing $t() calls' keys"]['nestedKeys']){
// If the "Existing $t() calls' keys" does not exist, create a new object
outputOfExtractedStrings["Existing $t() calls' keys"]['nestedKeys'] = {};
}
if(!outputOfExtractedStrings["Existing $t() calls' keys"]['nestedKeys'][pageKey]){
// If the pageKey does not exist, create a new object
outputOfExtractedStrings["Existing $t() calls' keys"]['nestedKeys'][pageKey] = [];
}
outputOfExtractedStrings["Existing $t() calls' keys"]['nestedKeys'][pageKey].push(pageTranslateValue);
}
}else{
// console.log(isLikelyVariableLike(strTrimmed));
// console.log(isUrl(strTrimmed));
// If the string is not in bracket notation, and NOT a URL -> add it to the output
if(strTrimmed && !isUrl(strTrimmed) && !isLikelyVariableLike(strTrimmed) && !harcode_skip_string_collection.includes(strTrimmed)){
if(!outputOfExtractedStrings["Existing $t() calls' keys"]['flatKeys']){
// If the "Existing $t() calls' keys" does not exist, create a new object
outputOfExtractedStrings["Existing $t() calls' keys"]['flatKeys'] = [];
}
outputOfExtractedStrings["Existing $t() calls' keys"]['flatKeys'].push(strTrimmed);
}
}
}else{
if (parsedResult) {
// If the string is in bracket notation, parse it to be:
// eg. TestPage: { "Hi {name}": "Hi {name}" }
const { objectName: pageKey, keyName:pageTranslateValue } = parsedResult;
// console.log(!isUrl(pageTranslateValue));
if(pageTranslateValue && !isUrl(pageTranslateValue) && !harcode_skip_string_collection.includes(pageTranslateValue)){
// NOTES: doesn't check isLikelyVariableLike(pageTranslateValue) here, because if parsedResult has value, most likely it is a key extracted from a $t() call when includeT is true
if(outputOfExtractedStrings[pageKey]){
// If the pageKey already exists, add the new key-value pair
// NTOES: replace(/\$\{(.*?)\}/g, (_, key) => `{${key}}`) is used to replace ${key} with {key} for consistency. ONLY value.
outputOfExtractedStrings[pageKey][pageTranslateValue] = transformBracketPlaceholders(pageTranslateValue.replace(/\$\{(.*?)\}/g, (_, key) => `{${key}}`));
}else{
// If the pageKey does not exist, create a new object
// replace(/\$\{(.*?)\}/g, (_, key) => `{${key}}`) is used to replace ${key} with {key} for consistency. ONLY value.
outputOfExtractedStrings[pageKey] = { [pageTranslateValue]: transformBracketPlaceholders(pageTranslateValue.replace(/\$\{(.*?)\}/g, (_, key) => `{${key}}`)) };
}
}
}else{
// console.log(isLikelyVariableLike(strTrimmed));
// console.log(isUrl(strTrimmed));
// If the string is not in bracket notation, and NOT a URL -> add it to the output
if(strTrimmed && !isUrl(strTrimmed) && !isLikelyVariableLike(strTrimmed) && !harcode_skip_string_collection.includes(strTrimmed)){
outputOfExtractedStrings[strTrimmed] = transformBracketPlaceholders(strTrimmed.replace(/\$\{(.*?)\}/g, (_, key) => `{${key}}`));
}
}
}
});
if (!fs.existsSync('src/locales')) {
fs.mkdirSync('src/locales');
}
fs.writeFileSync('src/locales/en.json', JSON.stringify(outputOfExtractedStrings, null, 2));
// Step 5: Output extractedStringsOfTranslationAsWholePointer, if any
if(extractedStringsOfTranslationAsWholePointer.size > 0) {
const outputOfExtractedStringsOfTranslationAsWholePointer = {};
Array.from(extractedStringsOfTranslationAsWholePointer).sort().forEach(strObj => {
// Check if the string is a bracket notation (e.g. TestPage['Hi {name}'])
// **NOTES: Since each page keeps its own translation key-value mapping,
// **NOTES: normally if we extracted exsiting $t() calls, we can assume it will be something like: $t("TestPage['Hi {name}']")
// **NOTES: In extractedStrings, we already extracted the string without the $t() call, so it will be like: "TestPage['Hi {name}']"
// **NOTES: If the string is in bracket notation, we will parse it to be: eg. TestPage: { "Hi {name}": "Hi {name}" }
const {result: str, hasTCall} = strObj;
// NOTES: since we already filter the extracted output in previous steps, we only meet hasTCall string if includeT is true
if( hasTCall && includeT) {
const extractedTKeys = extractAllTKeysFromString(str);
extractedTKeys.forEach(tKey => {
if(!outputOfExtractedStringsOfTranslationAsWholePointer["Existing $t() calls' keys for translate-as-whole-pointer"]){
outputOfExtractedStringsOfTranslationAsWholePointer["Existing $t() calls' keys for translate-as-whole-pointer"] = {};
}
const parsedResult = parseBracketNotation(tKey);
if (parsedResult) {
// If the string is in bracket notation, parse it to be:
// eg. TestPage: { "Hi {name}": "Hi {name}" }
const { objectName: pageKey, keyName:pageTranslateValue } = parsedResult;
const pageTranslateValueTrimmed = pageTranslateValue.trim();
if(!outputOfExtractedStringsOfTranslationAsWholePointer["Existing $t() calls' keys for translate-as-whole-pointer"]['nestedKeys']){
// If the flatKeys does not exist, create a new array
outputOfExtractedStringsOfTranslationAsWholePointer["Existing $t() calls' keys for translate-as-whole-pointer"]['nestedKeys'] = {};
}
if(!outputOfExtractedStringsOfTranslationAsWholePointer["Existing $t() calls' keys for translate-as-whole-pointer"]['nestedKeys'][pageKey]){
// If the pageKey does not exist, create a new array
outputOfExtractedStringsOfTranslationAsWholePointer["Existing $t() calls' keys for translate-as-whole-pointer"]['nestedKeys'][pageKey] = [];
}
// Add the pageTranslateValueTrimmed to the pageKey's set
outputOfExtractedStringsOfTranslationAsWholePointer["Existing $t() calls' keys for translate-as-whole-pointer"]['nestedKeys'][pageKey].push(pageTranslateValueTrimmed);
}else{
// If the string is not in bracket notation, add it to the output
if(!outputOfExtractedStringsOfTranslationAsWholePointer["Existing $t() calls' keys for translate-as-whole-pointer"]['flatKeys']){
// If the flatKeys does not exist, create a new array
outputOfExtractedStringsOfTranslationAsWholePointer["Existing $t() calls' keys for translate-as-whole-pointer"]['flatKeys'] = [];
}
outputOfExtractedStringsOfTranslationAsWholePointer["Existing $t() calls' keys for translate-as-whole-pointer"]['flatKeys'].push(tKey);
}
})
}else {
const parsedResult = parseBracketNotation(str);
if (parsedResult) {
// If the string is in bracket notation, parse it to be:
// eg. TestPage: { "Hi {name}": "Hi {name}" }
const { objectName: pageKey, keyName:pageTranslateValue } = parsedResult;
const pageTranslateValueTrimmed = pageTranslateValue.trim();
if(outputOfExtractedStringsOfTranslationAsWholePointer[pageKey]){
// If the pageKey already exists, add the new key-value pair
outputOfExtractedStringsOfTranslationAsWholePointer[pageKey][pageTranslateValueTrimmed] = transformBracketPlaceholders(pageTranslateValueTrimmed);
}else{
// If the pageKey does not exist, create a new object
outputOfExtractedStringsOfTranslationAsWholePointer[pageKey] = { [pageTranslateValueTrimmed]: transformBracketPlaceholders(pageTranslateValueTrimmed) };
}
}else{
// If the string is not in bracket notation, add it to the output
outputOfExtractedStringsOfTranslationAsWholePointer[str.trim()] = transformBracketPlaceholders(str.trim());
}
}
fs.writeFileSync('src/locales/en-TranslateAsWholePointer.json', JSON.stringify(outputOfExtractedStringsOfTranslationAsWholePointer, null, 2));
});
}
console.log(`✅ Extracted ${extractedStrings.size} strings → src/locales/en.json`);
console.log(`✅ Extracted ${extractedStringsOfTranslationAsWholePointer.size} strings → src/locales/en-TranslateAsWholePointer.json`);
}
function parseCustomizedComponentMapping(customizedComponentsTranslationMap, pageKeys){
if (!customizedComponentsTranslationMap){
// If no customized components mapping is provided, return null
return null;
}else{
// If has customized components mapping, parse it based on the pageKeys
const parsedMapping = {};
if(pageKeys?.length > 0) {
// If pageKeys is provided, filter the customizedComponentsTranslationMap based on the pageKeys
pageKeys.forEach(pageKey => {
const mappingOfCurPageKey = customizedComponentsTranslationMap[pageKey];
if(mappingOfCurPageKey){
Object.keys(mappingOfCurPageKey).forEach(componentName => {
const propsToExtract = mappingOfCurPageKey[componentName];
if(parsedMapping[componentName]){
// If the component already exists, add the new props to extract
parsedMapping[componentName] = new Set([...parsedMapping[componentName], ...propsToExtract]);
}else{
// If the component does not exist, create a new set of props to extract
parsedMapping[componentName] = new Set(propsToExtract);
}
});
}
});
}else{
// If no pageKeys is provided, parse the whole customizedComponentsTranslationMap
Object.keys(customizedComponentsTranslationMap).forEach( key => {
if(Array.isArray(customizedComponentsTranslationMap[key])){
// If The value is an array, it means the key is a component name and the value is an array of props to extract
const propsToExtract = customizedComponentsTranslationMap[key];
if(parsedMapping[key]){
// If the component already exists, add the new props to extract
parsedMapping[key] = new Set([...parsedMapping[key], ...propsToExtract]);
}else{
// If the component does not exist, create a new set of props to extract
parsedMapping[key] = new Set(propsToExtract);
}
}else{
const mappingOfCurPageKey = customizedComponentsTranslationMap[key];
if(mappingOfCurPageKey){
Object.keys(mappingOfCurPageKey).forEach(componentName => {
const propsToExtract = mappingOfCurPageKey[componentName];
if(parsedMapping[componentName]){
// If the component already exists, add the new props to extract
parsedMapping[componentName] = new Set([...parsedMapping[componentName], ...propsToExtract]);
}else{
// If the component does not exist, create a new set of props to extract
parsedMapping[componentName] = new Set(propsToExtract);
}
});
}
}
});
}
return parsedMapping;
}
}
function processVueFile(filePath, extractedStrings, extractedStringsOfTranslationAsWholePointer, includeT, customizedComponentsMapping, templateOnly = false) {
const content = fs.readFileSync(filePath, 'utf-8');
const { descriptor } = parse(content);
// 1. Process template
if (descriptor.template) {
const templateAst = compile(descriptor.template.content, { hoistStatic: false }).ast;
// fs.writeFileSync('src/locales/structure.json', JSON.stringify(templateAst,null, 2));
walkTemplateAst(templateAst, extractedStrings, extractedStringsOfTranslationAsWholePointer, includeT, customizedComponentsMapping);
}
// 2. Process script
// NOTES: If templateOnly is true, we only process the template, not the script
if ((descriptor.script || descriptor.scriptSetup) && !templateOnly) {
const scriptContent = descriptor.script?.content || descriptor.scriptSetup?.content;
processJSContent(scriptContent, extractedStrings, includeT);
}
}
function walkTemplateAst(node, extractedStrings, extractedStringsOfTranslationAsWholePointer, includeT, customizedComponentsMapping) {
// console.log('walkTemplateAst node:', node.type, node.content);
if (!node) return;
switch (node.type) {
// ROOT
case 0:
// ELEMENT
case 1:
// IF_BRANCH
case 10:{
const hasClassPropNamedTranslateAsWholePointer = node.props?.some(prop => {
// Check if the prop is a class or :class (type: 7) and contains translate-as-whole-pointer
// NOTES: ":class" need to be parsed as JSON array, so we need to replace single quotes with double quotes and use JSON.parse() to get the array of classes
if( prop.type === 7 && prop.rawName === ':class') {
// console.log(`Processing :class expression: ${prop.exp.content}`);
// check if anything need to be extracted from the :class expression,
// or any $t() calls in the :class expression
if (prop.exp?.content) {
// Trim whitespace/newlines first
const cleanExpr = prop.exp.content.trim();
walkClassPropsArray(cleanExpr, extractedStrings, includeT);
}
return containsClassViaRegexOrAST(prop.exp?.content ?? "", 'translate-as-whole-pointer', 'AST');
}else if( prop.name === 'class') {
// console.log(`Processing class expression: ${prop.value.content}`);
return containsClassViaRegexOrAST(prop.value?.content ?? "", 'translate-as-whole-pointer');
}
});
if( hasClassPropNamedTranslateAsWholePointer ) {
// Process the whole element as a single translation string
const {hasTCall, result} = processTranslationAsWholePointerClass(node, includeT);
// console.log('processTranslationAsWholePointerClass result:', result, 'hasTCall:', hasTCall);
if (((hasTCall && includeT) || !hasTCall) && result) {
extractedStringsOfTranslationAsWholePointer.add({result, hasTCall});
}
}else {
if(customizedComponentsMapping?.[node.tag]){
// console.log(`Found customized component: ${node.tag}`);
// Process customized component
const results = processCustomizedComponent(node, includeT, customizedComponentsMapping);
results.forEach(({result, hasTCall}) => {
// console.log("extractedStrings add 1:", result);
// extractedStrings.add(result);
extractedStrings.add(JSON.stringify(
{
result: result.trim(),
hasTCall: hasTCall
}
));
});
}
// Recurse into children: deep dive into the AST
if (node.children && node.children.length) {
node.children.forEach(child => walkTemplateAst(child, extractedStrings, extractedStringsOfTranslationAsWholePointer, includeT, customizedComponentsMapping));
}
}
break;
}
// Text Call
case 12: {
// Process text call nodes, deep dive into the node's content property
walkTemplateAst(node.content, extractedStrings, extractedStringsOfTranslationAsWholePointer, includeT, customizedComponentsMapping);
break;
}
// v-for
case 11: {
// Recurse into children: deep dive into the AST
if (node.children && node.children.length) {
node.children.forEach(child => walkTemplateAst(child, extractedStrings, extractedStringsOfTranslationAsWholePointer, includeT, customizedComponentsMapping));
}
break;
}
// If v-if, v-else-if, v-else
case 9: {
// Recurse into branches: deep dive into the AST for each if branch
if (node.branches && node.branches.length) {
node.branches.forEach(branch => {
// Extract string literals from branch.condition
if (branch.condition && branch.condition.content) {
walkVueIfCondition(branch.condition.content, extractedStrings, includeT);
}
// Recurse deeper into this branch
walkTemplateAst(branch, extractedStrings, extractedStringsOfTranslationAsWholePointer, includeT, customizedComponentsMapping);
});
}
break;
}
// COMPOUND_EXPRESSION: eg. "hi" + name, <div> hi {{ name }}</div>
case 8: {
// Process COMPOUND_EXPRESSION nodes
const {hasTCall, result} = processCompoundExpression(node, includeT);
if((hasTCall && includeT) || !hasTCall) {
result.forEach(res => {
// console.log("extractedStrings add 1:", res.trim());
// extractedStrings.add(res.trim());
extractedStrings.add(JSON.stringify(
{
result: res.trim(),
hasTCall: hasTCall // true if it contains $t() call
}
));
});
}
break;
}
// INTERPOLATION
case 5: { // INTERPOLATION
const results = processInterpolation(node, includeT);
results.forEach(({result, hasTCall}) => {
// console.log('Found INTERPOLATION: after 2', 'result:', result);
if (hasTCall) {
if (includeT) {
//NOTES: under this condition, the result the key of the $t() call: eg. {{ $t("Hello {name}") }} -> "Hello {name}"
// console.log("extractedStrings add 3:", result.trim());
// extractedStrings.add(result.trim());
extractedStrings.add(JSON.stringify(
{
result: result.trim(),
hasTCall: true
}
));
}
}else{
// if not a $t() call:
// check if the content is a { name } or similar
// if so -> skip it!
// **NOTES: normally for {{ name }}, we translate the assigned value for name in the script, not the template
// if not -> extract by adding in extractedStrings
if(!/^\{\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*\}$/.test(result)){
//NOTES: only possible result is a template literal, e.g. `Hello ${name}`
// console.log("extractedStrings add 4:", result.trim());
// extractedStrings.add(result.trim());
extractedStrings.add(JSON.stringify(
{
result: result.trim(),
hasTCall: false
}
));
}
}
});
break;
}
// TEXT node
case 2: {
// console.log('Found TEXT node:', node.content);
// Process TEXT nodes
const text = node.content.trim();
if (text) {
// console.log("extractedStrings add 5:", text);
// extractedStrings.add(text);
extractedStrings.add(JSON.stringify(
{
result: text,
hasTCall: false
}
));
}
break;
}
}
}
function walkClassPropsArray(cleanExpr, extractedStrings, includeT) {
// Parse expression string
try{
const code = `const _cls = ${cleanExpr};`;
const programAst = babelParser.parse(code, {
sourceType: 'module',
plugins: ['typescript']
});
// const exprAst = babelParser.parseExpression(cleanExpr);
// const programAst = {
// type: 'Program',
// body: [
// {
// type: 'ExpressionStatement',
// expression: exprAst,
// },
// ],
// sourceType: 'module',
// };
traverse(programAst, {
StringLiteral(path) {
// Skip object keys (these are the class names)
if (path.parent.type === 'ObjectProperty' && path.parent.key === path.node) {
// console.log(`Skipping class name key: ${path.node.value}`);
return;
}
// Skip string literals directly in array (static class names)
if (path.parent.type === 'ArrayExpression') {
// console.log(`Skipping static array string: ${path.node.value}`);
return;
}
// console.log(`Found string literal in condition: ${path.node.value}`);
// console.log("extractedStrings add 6:", path.node.value);
// extractedStrings.add(path.node.value);
extractedStrings.add(JSON.stringify(
{
result: path.node.value,
hasTCall: false // No $t() call in :class expressions
}
));
},
TemplateLiteral(path) {
const quasis = path.node.quasis;
const expressions = path.node.expressions;
let reconstructed = '';
for (let i = 0; i < quasis.length; i++) {
reconstructed += quasis[i].value.cooked;
if (i < expressions.length) {
const exprCode = generate(expressions[i]).code;
reconstructed += '${' + exprCode + '}';
}
}
// console.log("extractedStrings add 7:", arg.value);
// extractedStrings.add(reconstructed);
extractedStrings.add(JSON.stringify(
{
result: reconstructed,
hasTCall: false
}
));
},
CallExpression(path) {
// Check for $t(...) calls
const callee = path.node.callee;
const arg = path.node.arguments[0];
if(includeT){
if (callee?.type === 'Identifier' && callee?.name === '$t') {
if (arg && arg.type === 'StringLiteral') {
// console.log("extractedStrings add 8:", arg.value);
// extractedStrings.add(arg.value);
extractedStrings.add(JSON.stringify(
{
result: arg.value,
hasTCall: true
}
));
}
// ✅ Handle TemplateLiteral in $t(),
// eg. :class = "[
// {'dummy-webinar-video-card':curTab == $t(`EducationCenter['Webinars']`) && test == $t(`EducationCenter['test val {name}']`, {name: 'test'})},
// {'dummy-educational-video-card':curTab == 'Educational Videos'}
// ]";
// NOTES: different structure from other $t() CallExpression in the processJSContent function
if (arg.type === 'TemplateLiteral' && arg.expressions.length === 0) {
const raw = arg.quasis.map(q => q.value.cooked).join('');
// console.log("extractedStrings add 9:", raw);
// extractedStrings.add(raw);
extractedStrings.add(JSON.stringify(
{
result: raw,
hasTCall: true
}
));
}
}else if(callee?.property?.type === 'Identifier' && callee?.property?.name === '$t'){
// NOTES: in case, we still keep this logic which is the same as the one in processJSContent function
// ✅ Handle TemplateLiteral with no interpolations, eg.`tkeyTestWithWholeLiteralKKKK ${this.$t("TestPage['tTkeyTestWithWholeLiteral']")}`;
if (arg.type === 'TemplateLiteral' && arg.expressions.length === 0) {
const raw = arg.quasis.map(q => q.value.cooked).join('');
// console.log("extractedStrings add 10:", raw);
// extractedStrings.add(raw);
extractedStrings.add(JSON.stringify(
{
result: raw,
hasTCall: true
}
));
}
}
}
},
});
} catch (err) {
console.warn(`Failed of walkClassPropsArray: ${cleanExpr}`, err);
}
}
function walkVueIfCondition(exprStr, extractedStrings, includeT) {
try {
const code = `const _cls = ${exprStr};`;
const programAst = babelParser.parse(code, {
sourceType: 'module',
plugins: ['typescript']
});
// const exprAst = babelParser.parseExpression(exprStr);
// const programAst = {
// type: 'Program',
// body: [
// {
// type: 'ExpressionStatement',
// expression: exprAst,
// },
// ],
// sourceType: 'module',
// };
traverse(programAst, {
StringLiteral(path) {
// console.log("extractedStrings add 11:", path.node.value);
// extractedStrings.add(path.node.value);
extractedStrings.add(JSON.stringify(
{
result: path.node.value,
hasTCall: false
}
));
},
TemplateLiteral(path) {
const quasis = path.node.quasis;
const expressions = path.node.expressions;
let reconstructed = '';
for (let i = 0; i < quasis.length; i++) {
reconstructed += quasis[i].value.cooked;
if (i < expressions.length) {
const exprCode = generate(expressions[i]).code;
reconstructed += '${' + exprCode + '}';
}
}
// console.log("extractedStrings add 12:", reconstructed);
// extractedStrings.add(reconstructed);
extractedStrings.add(JSON.stringify(
{
result: reconstructed,
hasTCall: false
}
));
},
CallExpression(path) {
// Check for $t(...) calls
const callee = path.node.callee;
const arg = path.node.arguments[0];
if(includeT){
if (callee?.type === 'Identifier' && callee?.name === '$t') {
// console.log("extractedStrings add 7:", JSON.stringify(arg, null, 2));
if (arg && arg.type === 'StringLiteral') {
// console.log("extractedStrings add 13:", arg.value);
// extractedStrings.add(arg.value);
extractedStrings.add(JSON.stringify(
{
result: arg.value,
hasTCall: true
}
));
}
// ✅ Handle TemplateLiteral with no interpolations, eg.`tkeyTestWithWholeLiteralKKKK ${this.$t("TestPage['tTkeyTestWithWholeLiteral']")}`;
if (arg.type === 'TemplateLiteral' && arg.expressions.length === 0) {
const raw = arg.quasis.map(q => q.value.cooked).join('');
// console.log("extractedStrings add 14:", raw);
// extractedStrings.add(raw);
extractedStrings.add(JSON.stringify(
{
result: raw,
hasTCall: true
}
));
}
}else if(callee?.property?.type === 'Identifier' && callee?.property?.name === '$t'){
// ✅ Handle TemplateLiteral with no interpolations, eg.`tkeyTestWithWholeLiteralKKKK ${this.$t("TestPage['tTkeyTestWithWholeLiteral']")}`;
if (arg.type === 'TemplateLiteral' && arg.expressions.length === 0) {
const raw = arg.quasis.map(q => q.value.cooked).join('');
// console.log("extractedStrings add 15:", raw);
// extractedStrings.add(raw);
extractedStrings.add(JSON.stringify(
{
result: raw,
hasTCall: true
}
));
}
}
}
},
});
} catch (err) {
console.warn(`Failed to parse branch condition in walkVueIfBranches: ${exprStr}`, err);
}
}
function containsClassViaRegexOrAST(exprStr, targetClass, method = 'regex') {
// method can be 'regex' or 'AST'
// console.log('containsClassRegexOnly input:', exprStr, 'method:', method);
let hasClass = false;
if(method==="AST"){
try {
const cleanExpr = exprStr.trim();
const code = `const _cls = ${cleanExpr};`;
const programAst = babelParser.parse(code, {
sourceType: 'module',
plugins: ['typescript']
});
// const exprAst = babelParser.parseExpression(cleanExpr);
// const programAst = {
// type: 'Program',
// body: [
// {
// type: 'ExpressionStatement',
// expression: exprAst,
// },
// ],
// sourceType: 'module',
// };
traverse(programAst, {
StringLiteral(path) {
// Check object keys
if (path.parent.type === 'ObjectProperty' && path.parent.key === path.node) {
if (path.node.value === targetClass) {
hasClass = true;
}
}
// Check array literals
if (path.parent.type === 'ArrayExpression') {
if (path.node.value === targetClass) {
hasClass = true;
}
}
}
});
} catch (err) {
console.warn(`Failed to parse expression: ${exprStr}`, err);
}
}else{
// Split on whitespace to get individual classes
const classList = exprStr.split(/\s+/);
if (classList.includes(targetClass)) {
hasClass = true;
}
}
return hasClass;
}
function processCustomizedComponent(node, includeT, customizedComponentsMapping){
const results = [];
//get the all props need to be extracted from the customized component
const propsToExtract = customizedComponentsMapping[node.tag];
node.props?.forEach(prop =>{
if(prop.type === 7 && prop.rawName && propsToExtract?.has(prop.rawName.slice(1))){
//NOTES: rawName is the name of the prop, e.g. :title, :content, etc.
//NOTES: need to check the part after the ":", e.g. title, content, etc.
const propValue = prop.exp?.content;
// console.log('Found prop with value:', propValue);
const tKey = extractFirstTKeyFromString(propValue);
if(tKey) {
// If the prop value is a $t() call, add it to the results
if(includeT) {
results.push({
result: tKey,
hasTCall: true
});
// results.add(tKey);
}
}else if(/^`.*`\s*$/.test(propValue) || /^'.*'\s*$/.test(propValue)){
// If the prop value is a template literal or in format of 'xxx', add it directly
results.push({
result: propValue.slice(1,-1),
hasTCall: false,
});
// results.add(propValue.slice(1,-1));
}
//else: if the prop is simply a variable, e.g. title, content, etc. -> skip it! We will translate it in the script, not the template
}else if( propsToExtract?.has(prop.name)) {
const propValueNode = prop.value;
if(propValueNode?.type === 2) {
// If the prop value is a text node, add it directly
results.push({
result: propValueNode.content.trim(),
hasTCall: false,
});
// results.add(propValueNode.content.trim());
}
// ********* The following conditions won't happed *********************
// else if(propValueNode?.type === 5) {
// // If the prop value is an interpolation, process it
// const results = processInterpolation(propValueNode, includeT);
// results.forEach(({result, hasTCall}) => {
// if( ((hasTCall && includeT) || !hasTCall) && result) {
// results.push({
// result: result.trim(),
// hasTCall: hasTCall
// });
// // results.add(result.trim());
// }
// });
// }else if(propValueNode?.type === 8) {
// // If the prop value is a compound expression, process it
// const { hasTCall, result } = processCompoundExpression(propValueNode, includeT);
// if((hasTCall && includeT) || !hasTCall) {
// result.forEach(res => {
// results.push({
// result: res.trim(),
// hasTCall: hasTCall
// });
// // results.add(res.trim());
// });
// }
// }
}
});
return results;
}
function processTranslationAsWholePointerClass(node, includeT) {
// only need to check if the node type is 1;
// concate all text nodes in the children
// console.log('processTranslationAsWholePointerClass NODE:', node);
var result = '';
var hasTCall = false;
node.children?.forEach(child => {
// console.log('processTranslationAsWholePointerClass CHILD:', child);
if (typeof child === 'string') return; // skip raw spacing
if (child.type === 2) { // Text node
if(child.content) {
result += child.content;
}
// result += child.content;
} else if (child.type === 12) { // Text call
if ( child.content?.type === 2) {
if(child.content.content) {
result += child.content.content;
}
// result += child.content.content;
}else if (child.content?.type === 8) { // Compound expression
const { hasTCall:childHasTCall, result: childResult } = processCompoundExpression(child.content, includeT, true);
// console.log('type 12 processTranslationAsWholePointerClass type 8 childResult:', childResult);
//NOTES: normally the childResult will be an array of length 1, when run processCompoundExpression for translation-as-whole-pointer class
if((childHasTCall && includeT) || !childHasTCall) {
childResult.forEach(res => {
result += res;
});
}
// result += childResult;
if (childHasTCall){
hasTCall = true;
}
}else if (child.content?.type === 5) { // Interpolation
const childResults = processInterpolation(child.content, includeT, true);
// console.log('type 12 processTranslationAsWholePointerClass type 5 childResult:', childResults);
childResults.forEach(({hasTCall:childHasTCall, result: childResult}) => {
if(childResult) {
result += childResult;
}
// result += childResult;
if (childHasTCall){
hasTCall = true;
}
});
}
}else if(child.type === 8) { // Compound expression
const { hasTCall:childHasTCall, result: childResult } = processCompoundExpression(child, includeT, true);
// console.log('type 8 processTranslationAsWholePointerClass childResult:', childResult);
//NOTES: normally the childResult will be an array of length 1, when run processCompoundExpression for translation-as-whole-pointer class
if((childHasTCall && includeT) || !childHasTCall) {
childResult.forEach(res => {
result += res;
});
}
// result += childResult;
if (childHasTCall){
hasTCall = true;
}
}else if (child.type === 5) { // Interpolation
const childResults = processInterpolation(child, includeT, true);
// console.log('processTranslationAsWholePointerClass type 5 childResults:', childResults);
childResults.forEach(({hasTCall:childHasTCall, result: childResult}) => {
if(childResult) {
result += childResult;
}
// result += childResult;
if (childHasTCall){
hasTCall = true;
}
});
}else { // any other type
// we can recursively process its children
const {hasTCall:childHasTCall, result:childResult} = processTranslationAsWholePointerClass(child, includeT);
if (childResult) {
// Used for note the start of nest element content
result += "<";
result += childResult;
// Used for notes the end of nest element content
result += ">";
}
if (childHasTCall) {
hasTCall = true;
}
}
});
return {
hasTCall,
result
};
}
// type:8 -> Compound Epression eg. "hi" + name, <div> Hi {{ name }}</div>, <div> KK{{$t("HiKK { name }", {name: name})}} </div>, <div> {{button_name}} {{ name }}</div>,
function processCompoundExpression(node, includeT, isTranslationAsWholePointer = false) {
// var result = '';
var result = [];
var hasTCall = false;
node.children?.forEach(child => {
if (typeof child === 'string') return; // skip raw spacing
switch (child.type) {
case 2: // Text node in expression
// result += child.content.replace(/^'|'$/g, '');
result.push(child.content.replace(/^'|'$/g, ''));
break;
case 5: { // Interpolation: eg. {{ name }}
const childResults = processInterpolation(child, includeT, isTranslationAsWholePointer, false);
// console.log('processCompoundExpression childResults type 5:', childResults);
childResults.forEach(({hasTCall:childHasTCall, result: childResult}) => {
if( isTranslationAsWholePointer ){
// If isTranslationAsWholePointer: means we are processing a translation-as-whole-pointer class,
// so ANYWAY we need to add the result to the results array. For t calls, we will handle it later when add to extractedStringsOfTranslationAsWholePointer
result.push(childResult);
}else{
// If any childResult is hasTCall: we ONLY add t keys to the results array
// If NO childResult is hasTCall: we add all childResult to the results array
if( hasTCall ){
// NOTES: if hasTCall, means we have a $t() call in the previous parts.
// 1. If childHasTCall, means that childResult is a $t() call:
// -> we add it to the results array, when previous parts has a $t() call.
if( childHasTCall ){
result.push(childResult);
}
// 2. else : if no childHasTCall, means that childResult is not a $t() call,
// -> we do not add it to the results array, when previous parts has a $t() call.
}else{
//If NOT hasTCall, means we