UNPKG

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
// <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