UNPKG

vue-i18n-customized-extractor

Version:

A CLI tool to extract text and i18n keys from Vue.js files.

1,620 lines 79.8 kB
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 { load } = require('cheerio');
const MagicString = require('magic-string');

function replace(targetDir, translationMappingPath, options = {}) {
    // the first level key will be added when write $t() call to the file, eg. HomePage -> $t("HomePage[\"keyOne\"]")
    // NOTES: should be the one in i18n config file
    const curPageKey = options.translationPageKey; // this is for t call replacement
    const pageKeys = options.pageKeys || []; // this is for customized component mapping
    // whether it is processing the translationMapping for translation-as-whole-pointer
    const isTranslationAsWholePointer = options.isTranslationAsWholePointer || false;
    const excludeFiles = options.excludeFiles || [];
    const configFilePath = path.resolve(process.cwd(), options.configFilePath);
    const templateOnly = options.templateOnly;
    const vueScriptMode = options.vueScriptMode || "Options";
    // 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);

    // Step 1: Process translationMapping
    if (!fs.existsSync(translationMappingPath) || !translationMappingPath.endsWith('.json')) {
        console.warn(`⚠️ Translation mapping file does not exist or is not a .json file: ${translationMappingPath}`);
        return;
    }

    var originalTranslationMapping;
    try {
        originalTranslationMapping = JSON.parse(fs.readFileSync(translationMappingPath, 'utf-8'));
        console.log(`✅ Successfully loaded translation mapping from ${translationMappingPath}`);
    } catch (error) {
        console.error(`❌ Failed to parse translation mapping file: ${translationMappingPath}`, error.message);
        return;
    }
    const parsedTranslationMapping = parseTranslationMapping(originalTranslationMapping, isTranslationAsWholePointer);

    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 && !isTranslationAsWholePointer) {
            jsFiles = glob.sync(`${targetDir}/**/*.{js,ts}`, { ignore: ignorePatterns });
        }
    }

    console.log(`Found ${vueFiles.length} .vue files`);
    console.log(`Found ${jsFiles.length} .js/.ts files`);

    // Step 2: Process Vue files
    vueFiles.forEach(file => {
        replaceTextInVueFile(file, originalTranslationMapping, parsedTranslationMapping, customizedComponentsMapping, templateOnly, isTranslationAsWholePointer, curPageKey, vueScriptMode);
    });

    // Step 3: Process JS/TS files
    jsFiles.forEach(file => {
        replaceTextInJSFile(file, parsedTranslationMapping, curPageKey);
    });
}
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 parseTranslationMapping(translationMapping, isTranslationAsWholePointer = false) {
    const output = {};
    // extract the placeholders from the key
    if( isTranslationAsWholePointer ){
        Object.entries(translationMapping)
        .sort((a,b)=>{
            const aNum = parseInt(a[0]);
            const bNum = parseInt(b[0]);
            return aNum - bNum;
        }).forEach(([key, value]) => {
            // 1. parse key
            // NOTES: the key is definitely a string, so we can safely use it
            const actualKey = key.split('~~')[1];
            if (!actualKey) {
                console.warn(`⚠️ Skipping invalid key for translate-as-whole-pointer: ${key}`);
                return;
            }
            if(!output[actualKey]){
                output[actualKey] = {};
            }
            output[actualKey]['key'] = actualKey;
            const placeholdersInKey = extractPlaceholdersFromString(actualKey);
            output[actualKey]['keyPlaceholders'] = placeholdersInKey;
            // 2. parse value
            if (typeof value === 'string') {
                // If the value is a string, extract placeholders from it
                const placeholdersInValue = extractPlaceholdersFromString(value);
                if(!output[actualKey]['valuePlaceholders']){
                    output[actualKey]['valuePlaceholders'] = [];
                }
                output[actualKey]['valuePlaceholders'].push(...placeholdersInValue);
            }else{
                output[actualKey]['valuePlaceholders'] = [];
            }
        });
    } else {
        Object.entries(translationMapping).forEach(([key, value]) => {
            // 1. parse key
            // NOTES: the key is definitely a string, so we can safely use it
            if(!output[key]){
                output[key] = {};
            }
            output[key]['key'] = key;
            const placeholdersInKey = extractPlaceholdersFromString(key);
            output[key]['keyPlaceholders'] = placeholdersInKey;
            // 2. parse value
            if (typeof value === 'string') {
                // If the value is a string, extract placeholders from it
                const placeholdersInValue = extractPlaceholdersFromString(value);
                output[key]['valuePlaceholders'] = placeholdersInValue;
            }else{
                output[key]['valuePlaceholders'] = [];
            }
        });
    }
    return output;
}
function extractPlaceholdersFromString(str) {
    // Extract placeholders from a string,
    // E.g. "Hello {name}" -> ["name"]
    // E.g. "Hello {name} {age}" -> ["name", "age"]
    // NOTES: already handle the case of ${}. E.g. "Hello ${name} {age}" -> ["name", "age"]
    const matches = str.match(/\{([^}]+)\}|\$\{([^}]+)\}/g);
    if (!matches) return [];
    return matches.map(m => m.replace(/^\$\{/, '').replace(/^\{/, '').replace(/\}$/, '').trim());
}

function replaceTextInVueFile(filePath, originalTranslationMapping, parsedTranslationMapping, customizedComponentsMapping, templateOnly=false, isTranslationAsWholePointer, pageKey, vueScriptMode){
    const content = fs.readFileSync(filePath, 'utf-8');
    const { descriptor } = parse(content);
    const extractedStringsOffsetAndReplacementCollection = [];

    // 1. Process template
    if (descriptor.template) {
        const templateAst = compile(descriptor.template.content, { hoistStatic: false }).ast;
        // Start of <template> content in the .vue file
        const baseOffset = descriptor.template.loc.start.offset;
        const matchedKeysRelativeOffsetAndReplacementCollection = [];
        // get relative offsets and replacements from the template AST
        walkTemplateAstAndRecordMatch(templateAst, matchedKeysRelativeOffsetAndReplacementCollection, originalTranslationMapping, parsedTranslationMapping, customizedComponentsMapping, isTranslationAsWholePointer, pageKey);
        if (matchedKeysRelativeOffsetAndReplacementCollection.length > 0) {
            // Convert relative offsets to absolute offsets based on the template's start offset
            matchedKeysRelativeOffsetAndReplacementCollection.forEach(({ start, end, replacement }) => {
                extractedStringsOffsetAndReplacementCollection.push({
                    start: start + baseOffset,
                    end: end + baseOffset,
                    replacement
                });
            });
        }
    }

    // 2. Process script
    // NOTES: If templateOnly is true, we only process the template, not the script
    if ((descriptor.script || descriptor.scriptSetup) && !templateOnly && !isTranslationAsWholePointer) {
        const scriptContent = descriptor.script?.content || descriptor.scriptSetup?.content;
        // Start of <script> or <script setup> content in the .vue file
        const baseOffset = descriptor.script?.loc.start.offset || descriptor.scriptSetup?.loc.start.offset || 0;
        walkScriptContentInVueAndRecord(scriptContent, baseOffset, extractedStringsOffsetAndReplacementCollection, parsedTranslationMapping, pageKey, vueScriptMode);
    }

    // 3. use MagicString to replace the text based on extractedStringsOffsetAndReplacementCollection
    if (extractedStringsOffsetAndReplacementCollection.length > 0) {
        const magicString = new MagicString(content);
        extractedStringsOffsetAndReplacementCollection.forEach(({ start, end, replacement }) => {
            // console.log(`Replacing from ${start} to ${end} which is ${content.slice(start, end)} with "${replacement}"`);
            magicString.overwrite(start, end, replacement);
        });

        // Write the modified content back to the file
        fs.writeFileSync(filePath, magicString.toString(), 'utf-8');
        console.log(`✅ Replaced text in ${filePath}`);
    }
}

function walkTemplateAstAndRecordMatch(node, extractedStringsOffsetAndReplacementCollection, originalTranslationMapping, parsedTranslationMapping, customizedComponentsMapping, isTranslationAsWholePointer, pageKey) {
    // 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') {
                    // If not processing translation-as-whole-pointer, handle :class extraction for translation mapping
                    if( !isTranslationAsWholePointer ) {
                        // Calculate the absolute start offset of the :class expression within the entire file
                        
                        // 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();
                            const replacementResults = walkClassPropsArray(cleanExpr, parsedTranslationMapping, pageKey);
                            if (replacementResults.length > 0) {
                                const classExprStartOffset = prop.exp.loc.start.offset;
                                // If there are any replacements, add them to the collection with absolute offsets
                                replacementResults.forEach(replacementResult => {
                                    extractedStringsOffsetAndReplacementCollection.push({
                                        // absolute start and end offsets of the replacement in the file
                                        start: classExprStartOffset + replacementResult.start,
                                        end: classExprStartOffset + replacementResult.end,
                                        replacement: replacementResult.replacement
                                    });
                                });
                            }
                        }
                    }
                    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 ) {
                // If the node has a class or :class prop named translate-as-whole-pointer:
                // -> Process the node only if isTranslationAsWholePointer is true
                if (isTranslationAsWholePointer) {
                    // Process the whole element as a single translation string
                    const {hasTCall, result, start, end} = processTranslationAsWholePointerClass(node);
                    if (!hasTCall && result) {
                        const possibleKeyBase = result.trim();
                        if(parsedTranslationMapping[possibleKeyBase]){
                            const replacement = getTranslationAsWholePointerReplacement(node, possibleKeyBase, originalTranslationMapping, parsedTranslationMapping[possibleKeyBase], pageKey);
                            extractedStringsOffsetAndReplacementCollection.push({
                                start,
                                end,
                                replacement
                            });
                        }
                    }
                }
            }else {
                // If the node does not have a class or :class prop named translate-as-whole-pointer:
                // -> Process the node only if isTranslationAsWholePointer is false
                if( !isTranslationAsWholePointer ) {
                    if(customizedComponentsMapping?.[node.tag]){
                        // console.log(`Found customized component: ${node.tag}`);
                        // Process customized component
                        const outputOfPropsReplacements = processCustomizedComponent(node, parsedTranslationMapping, customizedComponentsMapping, pageKey);
                        extractedStringsOffsetAndReplacementCollection.push(...outputOfPropsReplacements);
                    }
                }
                // Recurse into children: deep dive into the AST
                if (node.children && node.children.length) { 
                    node.children.forEach(child => walkTemplateAstAndRecordMatch(child, extractedStringsOffsetAndReplacementCollection, originalTranslationMapping, parsedTranslationMapping, customizedComponentsMapping, isTranslationAsWholePointer, pageKey));
                }
            }
            break;
        }

        // Text Call
        case 12: {
            // Process text call nodes, deep dive into the node's content property
            walkTemplateAstAndRecordMatch(node.content, extractedStringsOffsetAndReplacementCollection, originalTranslationMapping, parsedTranslationMapping, customizedComponentsMapping, isTranslationAsWholePointer, pageKey);
            
            break;
        }

        // v-for
        case 11: {
            // Recurse into children: deep dive into the AST
            if (node.children && node.children.length) { 
                node.children.forEach(child => walkTemplateAstAndRecordMatch(child, extractedStringsOffsetAndReplacementCollection, originalTranslationMapping, parsedTranslationMapping, customizedComponentsMapping, isTranslationAsWholePointer, pageKey));
            }
            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 => {
                    if( !isTranslationAsWholePointer ) {
                        // Extract string literals from branch.condition
                        if (branch.condition && branch.condition.content) {
                            const replacementResults = walkVueIfCondition(branch.condition.content, parsedTranslationMapping, pageKey);
                            if (replacementResults.length > 0) {
                                const branchConditionExprStartOffset = branch.condition.loc.start.offset;
                                // If there are any replacements, add them to the collection with absolute offsets
                                replacementResults.forEach(replacementResult => {
                                    extractedStringsOffsetAndReplacementCollection.push({
                                        // absolute start and end offsets of the replacement in the file
                                        start: branchConditionExprStartOffset + replacementResult.start,
                                        end: branchConditionExprStartOffset + replacementResult.end,
                                        replacement: replacementResult.replacement
                                    });
                                });
                            }
                        }
                    }
                    // Recurse deeper into this branch
                    walkTemplateAstAndRecordMatch(branch, extractedStringsOffsetAndReplacementCollection, originalTranslationMapping, parsedTranslationMapping, customizedComponentsMapping, isTranslationAsWholePointer, pageKey)
                });
            }
            break;
        }

        // COMPOUND_EXPRESSION: eg. "hi" + name, <div> hi {{ name }}</div>
        case 8: {
            if( !isTranslationAsWholePointer ) {
                // Process COMPOUND_EXPRESSION nodes
                const {hasTCall, result:results, start, end} = processCompoundExpression(node);
                if(!hasTCall) {
                    results.forEach(res => {
                        const posiibleKey = res.trim();
                        const matchedKeyInfoInTranslationMapping = parsedTranslationMapping[posiibleKey];
                        if(matchedKeyInfoInTranslationMapping){
                            // If the possibleKey is found in the translation mapping, 
                            extractedStringsOffsetAndReplacementCollection.push({
                                // absolute start and end offsets of the replacement in the file
                                start: start,
                                end: end,
                                replacement:`{{ ${buildTCall(pageKey, matchedKeyInfoInTranslationMapping.key, matchedKeyInfoInTranslationMapping.keyPlaceholders, matchedKeyInfoInTranslationMapping.valuePlaceholders, 'template')} }} `
                            });
                        }
                        // console.log("extractedStrings add 1:", res.trim());
                        // extractedStrings.add(res.trim());
                    });
                }
            }
            break;
        }

        // INTERPOLATION
        case 5: { // INTERPOLATION
            if( !isTranslationAsWholePointer ) {
                const results = processInterpolation(node);
                results.forEach(({result, hasTCall, start, end}) => {
                    // console.log('Found INTERPOLATION: after 2', 'result:', result);
                    if (!hasTCall) {
                    // 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}`
                            const possibleKey = result.trim();
                            const matchedKeyInfoInTranslationMapping = parsedTranslationMapping[possibleKey];
                            if(matchedKeyInfoInTranslationMapping){
                                // If the possibleKey is found in the translation mapping, 
                                extractedStringsOffsetAndReplacementCollection.push({
                                    // absolute start and end offsets of the replacement in the file
                                    start: start,
                                    end: end,
                                    replacement: buildTCall(pageKey, matchedKeyInfoInTranslationMapping.key, matchedKeyInfoInTranslationMapping.keyPlaceholders, matchedKeyInfoInTranslationMapping.valuePlaceholders, 'template')
                                });
                            }
                            // console.log("extractedStrings add 4:", result.trim());
                            // extractedStrings.add(result.trim());
                        }            
                    }
                });
            }
            break;
        }


        // TEXT node
        case 2: {
            if( !isTranslationAsWholePointer ) {
                // console.log('Found TEXT node:', node.content);
                // Process TEXT nodes
                const text = node.content.trim();
                if (text) {
                    const possibleKey = text.trim();
                    const matchedKeyInfoInTranslationMapping = parsedTranslationMapping[possibleKey];
                    if(matchedKeyInfoInTranslationMapping){
                        // If the possibleKey is found in the translation mapping, 
                        extractedStringsOffsetAndReplacementCollection.push({
                            // absolute start and end offsets of the replacement in the file
                            start: node.loc.start.offset,
                            end: node.loc.end.offset,
                            replacement: `{{ ${buildTCall(pageKey, matchedKeyInfoInTranslationMapping.key, matchedKeyInfoInTranslationMapping.keyPlaceholders, matchedKeyInfoInTranslationMapping.valuePlaceholders, 'template')} }}`
                        });
                    }
                    // console.log("extractedStrings add 5:", text);
                    // extractedStrings.add(text);
                }
            }
            break;
        }
    
    }
}

function walkClassPropsArray(cleanExpr, parsedTranslationMapping, pageKey) {
    const outputOfExtractedStringsReplacementCollection = [];
    // Parse expression string
    try{
        const prefix = 'const _cls = ';
        const code = `${prefix}${cleanExpr};`;
        const programAst = babelParser.parse(code, {
            sourceType: 'module',
            plugins: ['typescript']
        });
        const offsetShift = prefix.length;
        // 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("extractedStrings add 6:", path.node.value);
                // extractedStrings.add(path.node.value);
                const possibleKey = path.node.value.trim();
                collectReplacementInfoForValuesInTag(path, possibleKey, parsedTranslationMapping, outputOfExtractedStringsReplacementCollection, pageKey, offsetShift);
            },
            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);
                const possibleKey = reconstructed.trim();
                collectReplacementInfoForValuesInTag(path, possibleKey, parsedTranslationMapping, outputOfExtractedStringsReplacementCollection, pageKey, offsetShift);
            },
            // 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
                //                 }
                //             ));
                //         }
                //     }
                // }
            // },
        });
        return outputOfExtractedStringsReplacementCollection;
    } catch (err) {
        console.warn(`Failed of walkClassPropsArray: ${cleanExpr}`, err);
    }
}

function walkVueIfCondition(exprStr, parsedTranslationMapping, pageKey) {
    const outputOfExtractedStringsReplacementCollection = [];
    try {
        const prefix = 'const _cls = ';
        const code = `${prefix}${exprStr};`;
        const programAst = babelParser.parse(code, {
            sourceType: 'module',
            plugins: ['typescript']
        });
        const offsetShift = prefix.length;

        // 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);
                const possibleKey = path.node.value.trim();
                collectReplacementInfoForValuesInTag(path, possibleKey, parsedTranslationMapping, outputOfExtractedStringsReplacementCollection, pageKey, offsetShift);
            },
            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);
                const possibleKey = reconstructed.trim();
                collectReplacementInfoForValuesInTag(path, possibleKey, parsedTranslationMapping, outputOfExtractedStringsReplacementCollection, pageKey, offsetShift);
            },
            // 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 
            //                     }
            //                 ));
            //             }
            //         }
            //     }
            // },
        });
        return outputOfExtractedStringsReplacementCollection;
    } 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 prefix = 'const _cls = ';
            const code = `${prefix}${cleanExpr};`;
            const programAst = babelParser.parse(code, {
                sourceType: 'module',
                plugins: ['typescript']
            });
            const offsetShift = prefix.length;

            // NOTES: the way below cannot cover all dynamic class value formats.
            // 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, parsedTranslationMapping, customizedComponentsMapping, pageKey){
    const outputOfPropsReplacementCollection = [];
    //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 && (/^`.*`\s*$/.test(propValue) || /^'.*'\s*$/.test(propValue))){
                // If the prop value is a template literal or in format of 'xxx', check if it is in translation mapping
                const possibleKey = propValue.slice(1,-1).trim();
                const matchedKeyInfoInTranslationMapping = parsedTranslationMapping[possibleKey];
                if(matchedKeyInfoInTranslationMapping) {
                    const start = prop.exp.loc.start.offset;
                    const end = prop.exp.loc.end.offset;
                    outputOfPropsReplacementCollection.push({
                        start: start,
                        end: end,
                        replacement: buildTCall(pageKey, matchedKeyInfoInTranslationMapping.key, matchedKeyInfoInTranslationMapping.keyPlaceholders, matchedKeyInfoInTranslationMapping.valuePlaceholders, 'templateTag')
                    });
                }
                // 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, replace value and prop name if has matched key in translation mapping
                const possibleKey = propValueNode.content.trim();
                const matchedKeyInfoInTranslationMapping = parsedTranslationMapping[possibleKey];
                if(matchedKeyInfoInTranslationMapping) {
                    const start = prop.value.loc.start.offset;
                    const end = prop.value.loc.end.offset;
                    outputOfPropsReplacementCollection.push({
                        start: start,
                        end: end,
                        // wrap in quotes
                        replacement: `"${buildTCall(pageKey, matchedKeyInfoInTranslationMapping.key, matchedKeyInfoInTranslationMapping.keyPlaceholders, matchedKeyInfoInTranslationMapping.valuePlaceholders, 'templateTag')}"`
                    });

                    // since it is changed to be a $t() call, we need to replace prop name. eg. title = "Hi" -> :title = "$t('Hi')"
                    const propNameStart = prop.loc.start.offset;
                    // `prop.name.length` = length of 'title' = 5
                    const propNameEnd = propNameStart + prop.name.length;
                    outputOfPropsReplacementCollection.push({
                        start: propNameStart,
                        end: propNameEnd,
                        replacement: `:${prop.name}` // add ":" to the prop name
                    });
                }

                // results.add(propValueNode.content.trim());
            }
        }
    });
    return outputOfPropsReplacementCollection;
}

function processTranslationAsWholePointerClass(node) {
// 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, 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
                childResult.forEach(res => {
                    result += res;
                });
                // result += childResult;
                if (childHasTCall){
                    hasTCall = true;
                }
            }else if (child.content?.type === 5) { // Interpolation
                const childResults = processInterpolation(child.content, 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, 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
            childResult.forEach(res => {
                result += res;
            });
            // result += childResult;
            if (childHasTCall){
                hasTCall = true;
            }
        }else if (child.type === 5) { // Interpolation
            const childResults = processInterpolation(child, 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);
            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,
        start: node.loc.start.offset,
        end: node.loc.end.offset
    };
}


// 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, 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, 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 do not have a $t() call in the previous parts.
                            // 1. If current childResult has a $t() call,
                            // -> we reset the result to only include the current childResult, and will only add other t keys in later loop.
                            if( childHasTCall ){
                                result = [childResult];
                            }else{
                            // 2. If current childResult does not have a $t() call,
                            // -> we add it to the results array, when previous parts has no $t() call.
                                result.push(childResult);
                            }
                        }

                    }
                    //update hasTCall if childHasTCall is true, for next loop
                    if (childHasTCall) {
                        hasTCall = true;
                    }
                });
                break;
            }
        
            default:
                // Unknown type — skip or handle if needed
                break;
        }
    });

    if(isTranslationAsWholePointer || !hasTCall) {
    // If isTranslationAsWholePointer or not hasTCall, we need to return the whole content as it is
        result = [result.join('')]; // join all parts together
    }

    const firstChild = node.children[0];
    const lastChild = node.children[node.children.length - 1];
    const innerStart = firstChild.loc.start.offset;
    const innerEnd = lastChild.loc.end.offset;

    return {
        hasTCall,
        result,
        start: innerStart,
        end: innerEnd
    };
}

// type: 5 -> Interpolation: eg. {{ name }},
function processInterpolation(node, isTranslationAsWholePointer = false, checkTenaryExpression = true) {
    var results = [];
    // var hasTCall = false;
    const contentNode = node.content;
    const rawContent = node.loc.source.trim();
    const innerStartOffset = contentNode.loc.start.offset;
    const innerEndOffset = contentNode.loc.end.offset;

    if (contentNode.type === 4) {
    // if not a template literal, but a simple expression like {{ name }}, or {{ $t("Hello") }}
    //eg. {{ name }} -> return as '{ name }'
        // expr: Parsed, trimmed version of what was inside {{ ... }} → no {{ }} anymore
        const expr = contentNode.content;
        
        // NOTES: under this condition, the expr is a simple expression, e.g. name, $t("Hello"), etc.
        // NOTES: so if the expr is a $t() call, it will be only one $t() call, not multiple $t() calls
        const extractedResultsByHandlingTernaryExpression = extractStringsViaHandlingTernaryExpression(expr);
        // console.log('processInterpolation expr:', expr, 'extractedResultsByHandlingTernaryExpression:', extractedResultsByHandlingTernaryExpression);
        if(isTranslationAsWholePointer){
            var hasTCall = false;
            const tKey = extractFirstTKeyFromString(expr);
            if (tKey) {
                hasTCall = true;
            }
            result = rawContent.slice(1, -1); // return the whole content as it is
            // console.log('extractedResultsByHandlingTernaryExpression 1', result);
            results.push({result, hasTCall});
        }else if(extractedResultsByHandlingTernaryExpression.length === 0 || !checkTenaryExpression) {
            // console.log('extractedResultsByHandlingTernaryExpression 2', expr);
            const tKey = extractFirstTKeyFromString(expr);
            var result = '';
            var hasTCall = false;
            // console.log('processInterpolation expr:', expr, tKey);
            // Update hasTCall based on whether tKey is found
            if (tKey) {
                hasTCall = true;
            }

            if (/^`.*`\s*$/.test(expr)) {
            // if the content is a template literal (e.g. {{`Hello ${name}`}})
                // → extract
                // console.log('processInterpolation expr:', expr, 'tKey:', tKey);
                if (tKey) {
                    // hasTCall = true;
                    result = `${tKey}`;
                } else {
                    result = `${expr.slice(1, -1)}`; // remove the backticks
                }
            }else{
            // if the content is a simple expression (e.g. {{ name }})
                // → return as '{ name }'
                // console.log('processInterpolation expr:', expr, 'tKey:', tKey);
                if (tKey) {
                    // hasTCall = true;
                    result = `${tKey}`;
                } else {
                    // replace {{ name }} with { name }, {{name}} with {name}
                    result = rawContent.slice(1, -1);
                }
            }
            results.push({result, hasTCall, start: innerStartOffset, end: innerEndOffset});
        }else{
            // If the expr is a ternary expression, e.g. {{ name ? 'Hello' : 'Hi' }}
            // or a complex expression, e.g. {{ $t("Hello {name}", {name: name}) }}
            // we will extract the strings from the ternary expression
            extractedResultsByHandlingTernaryExpression.forEach( ({content:expr, isLiteralLikeValue, relativeStart, relativeEnd}) => {
                // console.log('extractedResultsByHandlingTernaryExpression 3:', expr, 'isLiteralLikeValue:', isLiteralLikeValue);
                if(isLiteralLikeValue) {
                    const tKey = extractFirstTKeyFromString(expr);
                    var result = '';
                    var hasTCall = false;
                    // console.log('extractedResultsByHandlingTernaryExpression:', expr, tKey);
                    // Update hasTCall based on whether tKey is found
                    if (tKey) {
                        hasTCall = true;
                    }

                    if (/^`.*`\s*$/.test(expr)) {
                    // if the content is a template literal (e.g. {{`Hello ${name}`}})
                        // → already extracted as `Hello ${name}`
                        // console.log('processInterpolation expr:', expr, 'tKey:', tKey);
                        // if the expr is a $t() call, add $t() call key to the result
                        if (tKey) {
                            // hasTCall = true;
                            result = `${tKey}`;
                        } else {
                        // if the expr is not a $t() call, just remove the backticks and return
                            result = `${expr.slice(1, -1)}`; // remove the backticks
                        }
                    }else{
                    // if the content is a simple expression (e.g. {{ name }})
                        // → return as '{ name }'
                        // console.log('processInterpolation expr:', expr, 'tKey:', tKey);
                        // console.log('extractedResultsByHandlingTernaryExpression: simple expression', expr, tKey, rawContent);
                        if (tKey) {
                            // hasTCall = true;
                            result = `${tKey}`;
                        } else {
                            // replace {{ name }} with { name }, {{name}} with {name}
                            result = expr.slice(1, -1);
                        }
                    }
                    results.push({result, hasTCall, start: innerStartOffset + relativeStart, end: innerStartOffset + relativeEnd});
                }
            });
        }
        
    }
    // console.log('processInterpolation results:', node.content.content, results);
    return results;

}

function extractFirstTKeyFromString(str) {
// NOTES: This function extracts the first string parameter from the first $t() call in a string.
    // Check if the expression is a $t() call
    const match = str.match(/\$t\s*\(\s*(['"`])((?:\\\1|.)*?)\1/);
    return match ? match[2] : null;
}

function extractStringsViaHandlingTernaryExpression(expression) {
    const result = [];
    // console.log('extractStringsViaHandlingTernaryExpression expression:', expression);
    try {
        const ast = babelParser.parseExpression(expression);
        // console.log('Found ConditionalExpression:',expression, ast);
        traverse({
            type: 'File',
            program: {
            type: 'Program',
            body: [{ type: 'ExpressionStatement', expression: ast }],
            sourceType: 'module',
            }
        }, {
            ConditionalExpression(path) {
                // console.log('Found ConditionalExpression:',expression);
                const { consequent, alternate } = path.node;
                [consequent, alternate].forEach(part => {
                    // Skip raw variable identifiers (like button_name)
                    // Only extract if it's a literal-like value
                    const raw = expression.slice(part.start, part.end);
                    result.push({
                        content:raw, 
                        isLiteralLikeValue: isLiteralLikeValue(part),
                        relativeStart: part.start,
                        relativeEnd: part.end
                    });
                });
            },
            LogicalExpression(path) {
                if (path.node.operator === '??') {
                    const right = path.node.right;
                    // console.log('Found LogicalExpression with ?? operator:',expression, isLiteralLikeValue(right));

                    // Skip raw variable identifiers (like button_name)
                    // Only extract if it's a literal-like value
                    const raw = expression.slice(right.start, right.end);
                    result.push({
                        content:raw, 
                        isLiteralLikeValue: isLiteralLikeValue(right),
                        relativeStart: right.start,
                        relativeEnd: right.end
                    });
                }
            }
        });
    } catch (e) {
        return [];
    }
    return result; // Return the original expression if no ternary found
}
// ✅ Only extract if it's a literal-like value (for ternary expressions)
function isLiteralLikeValue(node) {
    
    const output = (
      node.type === 'StringLiteral' ||
      node.type === 'NumericLiteral' ||
      node.type === 'TemplateLiteral' ||
      (node.type === 'CallExpression' &&
       node.callee?.type === 'Identifier' &&
       node.callee.name === '$t')
    );
    // console.log('isLiteralLikeValue node:', output);
    return output;
}

function walkScriptContentInVueAndRecord(scriptContent, baseOffset, extractedStringsOffsetAndReplacementCollection, parsedTranslationMapping, pageKey, vueScriptMode) {
    const matchedKeysReplacementsCollection = getMatchedKeysReplacementsCollectionInScript(scriptContent, parsedTranslationMapping, pageKey);
    // console.log(`matchedKeysReplacementsCollection: ${JSON.stringify(matchedKeysReplacementsCollection, null, 2)}`);
    matchedKeysReplacementsCollection.forEach(({start:relativeStart, end:relativeEnd, replacement}) => {
        extractedStringsOffsetAndReplacementCollection.push({
            start: relativeStart + baseOffset,
            end: relativeEnd + baseOffset,
            replacement: vueScriptMode == "Options" ? `this.$${replacement}` : replacement,
        });
    });
}
function replaceTextInJSFile(filePath, parsedTranslationMapping, pageKey) {
    // 1. Read the file content 
    const scriptContent = fs.readFileSync(filePath, 'utf-8');

    // 2. Parse the script content to get the matched keys replacements collection
    const matchedKeysReplacementsCollection = getMatchedKeysReplacementsCollectionInScript(scriptContent, parsedTranslationMapping, pageKey);

    // 3. Use magicString to replace the matched keys in the script content based matchedKeysReplacementsCollection
    const magicString = new MagicString(scriptContent);
    matchedKeysReplacementsCollection.forEach(({start, end, replacement}) => {
        magicString.overwrite(start, end, replacement);
    });
    fs.writeFileSync(filePath, magicString.toString(), 'utf-8');
    console.log(`Replaced text in ${filePath} with matched keys from translation mapping for page: ${pageKey}`);
}
function getMatchedKeysReplacementsCollectionInScript(code, parsedTranslationMapping, pageKey) {
    const outputOfMatchedKeysReplacementsCollection = [];
   
    const ast = babelParser.parse(code, {
        sourceType: 'module',
        plugins: ['typescript', 'jsx']
    });
    // fs.writeFileSync('src/locales/structure.json', JSON.stringify(ast,null, 2));

    traverse(ast, {
        StringLiteral(path) {
            const parent = path.parent;

            // Ignore imports
            if (parent.type === 'ImportDeclaration') {
                return;
            }
            if (parent.type === 'ImportSpecifier' || parent.type === 'ImportDefaultSpecifier') {
                return;

            }

            // Skip: window.open('https://example.com') and window.open('https://example.com', '_blank')
            if (
                parent.callee?.type === 'MemberExpression' &&
                parent.callee?.object?.name === 'window' &&
                parent.callee?.property?.name === 'open'
            ) {
                return;
            }

            // Skip: importLibrary('places')
            if (
                parent.type === 'CallExpression' &&
                parent.callee.type === 'MemberExpression' &&
                parent.callee.property?.name === 'importLibrary'
            ) {
                return;
            }

            //Skip import('some-module')
            if (
                parent.type === 'CallExpression' &&
                parent.callee.type === 'Import'
            ) {
                return;
            }

            // Ignore strings in console.log(...)
            if (
                parent.type === 'CallExpression' &&
                parent.callee.type === 'MemberExpression' &&
                parent.callee.object.name === 'console' &&
                parent.callee.property.name === 'log'
            ) {
                return;
            }

            // Skip emits: ['close']
            if (
                parent.type === 'ArrayExpression' &&
                (
                    path.findParent(p => 
                        p.isObjectProperty() && p.node.key.name === 'emits'
                    )
                )
            ) {
                // console.log("Exclude emits: ['close']");
                return;
            }

            // Skip $emit() or emit() calls
            if (
                parent.type === 'CallExpression' &&
                (
                  (parent.callee.type === 'MemberExpression' &&
                    (parent.callee.property?.name === '$emit' || parent.callee.property?.name === 'emit')) || // this.emit / this.$emit
                  (parent.callee.type === 'Identifier' &&
                    (parent.callee.name === '$emit' || parent.callee.name === 'emit'))              // just emit()
                )
            ) {
                return;
            }

            // Skip $refs usage
            if(path.findParent(p => {
                const node = p.node;
                return (
                node.type === 'MemberExpression' &&
                (
                    // Matches: this.$refs['something']
                    (node.object?.type === 'MemberExpression' &&
                    node.object.object?.type === 'ThisExpression' &&
                    node.object.property?.name === '$refs') ||

                    // Matches: $refs['something']
                    (node.object?.type === 'Identifier' && node.object.name === '$refs')
                )
                );
            })){
                return;
            }

            // Skip name: 'SomeName'
            if (
                path.parent.type === 'ObjectProperty' &&
                path.parent.key &&
                ((path.parent.key.type === 'Identifier' && path.parent.key.name === 'name') ||
                (path.parent.key.type === 'StringLiteral' && path.parent.key.value === 'name'))
            ) {
                return;
            }
  
            
            // Skip mapState, mapActions usage
            if (
                path.findParent(p => 
                    p.isCallExpression() &&
                    ['mapState', 'mapActions', 'mapGetters', 'mapMutations'].includes(p.node.callee.name)
                )
            ) {
                return;
            }

            // Skip $router, this.$router(), router, useRouter(), $route, this.$route, route, useRoute()
            if (
                path.findParent(p => {
                  const node = p.node;
              
                  if (
                    node.type === 'CallExpression' &&
                    node.callee?.type === 'MemberExpression' &&
                    node.callee.object?.type === 'Identifier' &&
                    ['router', '$router', 'route', '$route'].includes(node.callee.object.name)
                  ) {
                    return true;
                  }
                  
                  // Also handle this.$router.push(), this.$route.someMethod()
                  if (
                    node.type === 'CallExpression' &&
                    node.callee?.type === 'MemberExpression' &&
                    node.callee.object?.type === 'MemberExpression' &&
                    node.callee.object.object?.type === 'ThisExpression' &&
                    (node.callee.object.property?.name === '$router' || node.callee.object.property?.name === '$route')
                  ) {
                    return true;
                  }
                  
                  // Optionally, if you still want to catch direct useRouter().push() or useRoute().someMethod() (less common)
                  if (
                    node.type === 'CallExpression' &&
                    node.callee?.type === 'MemberExpression' &&
                    node.callee.object?.type === 'CallExpression' &&
                    node.callee.object.callee?.type === 'Identifier' &&
                    ['useRouter', 'useRoute'].includes(node.callee.object.callee.name)
                  ) {
                    return true;
                  }
              
                  return false;
                })
            ) {
                return;
            }

            // check $t() call
            if (parent.callee?.property?.type === 'Identifier' && parent.callee?.property?.name === '$t') {
                return;
            }

            // Otherwise, check match and record replacement data
            // console.log("extractedStrings add 17:", path.node.value);
            // extractedStrings.add(path.node.value);
            const possibleKey = path.node.value.trim();
            const matchedKeyDataInTranslationMapping = parsedTranslationMapping[possibleKey];
            if (matchedKeyDataInTranslationMapping) {
                outputOfMatchedKeysReplacementsCollection.push({
                    start: path.node.start,
                    end: path.node.end,
                    replacement: buildTCall(pageKey, matchedKeyDataInTranslationMapping.key, matchedKeyDataInTranslationMapping.keyPlaceholders, matchedKeyDataInTranslationMapping.valuePlaceholders, 'script')
                });
            }
        },
        // CallExpression(path) {
        //     const callee = path.node.callee;
        //     const arg = path.node.arguments[0];
        //     // console.log('Found $t() call 111111:', includeT, path.node.callee.property?.name, path.node.callee.property?.type, path.node.arguments[0]);
        //     if(includeT){
        //         if (callee?.type === 'Identifier' && callee?.name === '$t') {
        //             if (arg && arg.type === 'StringLiteral') {
        //                 // console.log("extractedStrings add 18:", arg.value);
        //                 // extractedStrings.add(arg.value);
        //                 extractedStrings.add(JSON.stringify(
        //                     {
        //                         result: arg.value,
        //                         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 19:", raw);
        //                 // extractedStrings.add(raw);
        //                 extractedStrings.add(JSON.stringify(
        //                     {
        //                         result: raw,
        //                         hasTCall: true 
        //                     }
        //                 ));
        //             }
        //         }
        //     }
            
        // },
        TemplateLiteral(path) {
            const parent = path.parent;
            // console.log('Found $t() call 111111:',code.slice(path.node.start, path.node.end), parent.type);
            // Only extract if used in contexts like return, assignment, etc.
            if (
                parent.type === 'ReturnStatement' ||
                parent.type === 'VariableDeclarator' ||
                parent.type === 'ArrowFunctionExpression' ||
                // a = `is ${varB}`
                parent.type === 'VariableDeclarator' ||
                // reassigning a variable. eg. var a = `is ${varB}`; a = `is ${varB} C`;
                parent.type === 'AssignmentExpression' ||
                // a ? "b" : "c"
                parent.type === 'ConditionalExpression' ||
                // a?.b ?? "c"
                parent.type === 'LogicalExpression' 
            ) {
                // Check if it has any expressions (i.e. uses ${...})
                if (path.node.expressions.length > 0) {
                    const { start, end } = path.node;
                    const raw = code.slice(start, end); // This preserves the original `${...}` syntax
                    const tKey = extractFirstTKeyFromString(raw);
                    if (!tKey) {
                        // If it's just a template literal without $t(), add the raw content
                        const possibleKey = raw.slice(1, -1).trim();
                        const matchedKeyDataInTranslationMapping = parsedTranslationMapping[possibleKey];
                        if (matchedKeyDataInTranslationMapping) {
                            outputOfMatchedKeysReplacementsCollection.push({
                                start: start,
                                end: end,
                                replacement: buildTCall(pageKey, matchedKeyDataInTranslationMapping.key, matchedKeyDataInTranslationMapping.keyPlaceholders, matchedKeyDataInTranslationMapping.valuePlaceholders, 'script')
                            });
                        }
                        // console.log("extractedStrings add 21:", raw.slice(1, -1)); 
                        // extractedStrings.add(raw.slice(1, -1)); // Remove the backticks
                    }
                }
            }
        }
    });
    // console.log(`getMatchedKeysReplacementsCollectionInScript: ${JSON.stringify(outputOfMatchedKeysReplacementsCollection, null, 2)}`);
    return outputOfMatchedKeysReplacementsCollection;
}

function collectReplacementInfoForValuesInTag(path, possibleKey, parsedTranslationMapping, output, pageKey, offsetShift = 0) {
    const matchedKeyDataInTranslationMapping = parsedTranslationMapping[possibleKey];
    // relative to prop.exp.content
    if (matchedKeyDataInTranslationMapping) {
        const babelRelativeStart = path.node.start;
        const babelRelativeEnd = path.node.end;
        output.push({
            start: babelRelativeStart - offsetShift,
            end: babelRelativeEnd - offsetShift,
            replacement: buildTCall(pageKey, matchedKeyDataInTranslationMapping.key, matchedKeyDataInTranslationMapping.keyPlaceholders, matchedKeyDataInTranslationMapping.valuePlaceholders, 'templateTag'),
        });
    }
}

function buildTCall(pageKey, key, keyPlaceholders, valuePlaceholders, contextType = 'template') {
    if(keyPlaceholders.length !== valuePlaceholders.length) {
        console.warn(`⚠️ Mismatched placeholders for key: ${key}. Key placeholders: ${keyPlaceholders}, Value placeholders: ${valuePlaceholders}`);
        return null;
    }
    // Build the $t() call
    if(keyPlaceholders.length === 0 && valuePlaceholders.length === 0) {
        if(contextType === 'templateTag') {
            // use single quotes for template tags replacement
            return pageKey ? `$t('${pageKey}[\\\'${key}\\\']')` : `$t('${key}')`;
        }else if(contextType === 'template') {
            return pageKey ? `$t("${pageKey}[\\\"${key}\\\"]")` : `$t("${key}")`;
        } else {
            return pageKey ? `t("${pageKey}[\\\"${key}\\\"]")` : `t("${key}")`;
        }
    }else if(keyPlaceholders.length > 0 && valuePlaceholders.length > 0) {
        const tCallParams = buildTCallParams(keyPlaceholders, valuePlaceholders);
        if(contextType === 'templateTag') {
            // use single quotes for template tags replacement
            return pageKey ? `$t('${pageKey}[\\\'${key}\\\']', { ${tCallParams} })` : `$t('${key}', { ${tCallParams} })`;
        }else if(contextType === 'template') {
            return pageKey ? `$t("${pageKey}[\\\"${key}\\\"]", { ${tCallParams} })` : `$t("${key}", { ${tCallParams} })`;
        }else {
            return pageKey ? `t("${pageKey}[\\\"${key}\\\"]", { ${tCallParams} })` : `t("${key}", { ${tCallParams} })`;
        }
    }
    return tCall;
}
function buildTCallParams(keyPlaceholders, valuePlaceholders) {
    const map = {};
    keyPlaceholders.forEach((placeholder, index) => {
        map[placeholder] = valuePlaceholders[index];
    });
    return Object.entries(map).map(([k, v]) => `${v}: ${k}`).join(", ");
}
function getTranslationAsWholePointerReplacement(node, keyBase, originalMappings, matchedKeyInfoInParsedTranslationMapping, pageKey) {
    // Process target code
    const $ = load(node.loc.source, { decodeEntities: false, xmlMode: true });

    // Find the first tag node — assumes `oldHTML` contains only one root element
    const rootNode = $.root().children().toArray().find(n => n.type === 'tag');

    if (!rootNode) {
    throw new Error('No root tag found in oldHTML');
    }

    const tagName = rootNode.name;
    const attribs = Object.entries(rootNode.attribs || {})
        .map(([k, v]) => `${k}="${v}"`)
        .join(' '); 

    // Now select that tag dynamically and Get all its inner nodes
    const rootNodes = $(`${tagName}`).contents().toArray();

    const matchedKeys = Object.keys(originalMappings).filter(key => {
        const actualKeyBase = key.split('~~')[1];
        if (!actualKeyBase) return false;
        return actualKeyBase === keyBase;
    });

    matchedKeys.sort((a, b) => {
        const aIdx = parseInt(a.match(/^(\d+)/)[1], 10);
        const bIdx = parseInt(b.match(/^(\d+)/)[1], 10);
        return aIdx - bIdx;
    });

    if (matchedKeys.length === 0) {
        console.warn(`⚠️ No matching keys for block (baseKey): "${keyBase}"`);
        return;
    }

    let result = '';
    let matchedKeyIdx = 0;

    matchedKeys.forEach((key) => {
        const actualKeyBase = key.split('~~')[1];
        if (!actualKeyBase) {
            console.warn(`⚠️ Skipping invalid key for translate-as-whole-pointer: ${key}`);
            return;
        }

        const isNested = key.includes('Nested');
        const tCall = buildTCall(pageKey, key, matchedKeyInfoInParsedTranslationMapping.keyPlaceholders, matchedKeyInfoInParsedTranslationMapping.valuePlaceholders, 'template');

        if (!tCall) {
            console.warn(`⚠️ Failed to build $t() for key: ${key}`);
            return;
        }

        if (isNested) {
            let tagNode = null;
            while (matchedKeyIdx < rootNodes.length) {
            if (rootNodes[matchedKeyIdx].type === 'tag') {
                tagNode = rootNodes[matchedKeyIdx];
                matchedKeyIdx++;
                break;
            }
            matchedKeyIdx++;
            }
            if (tagNode) {
                const tagName = tagNode.name;
                const attribs = Object.entries(tagNode.attribs || {})
                    .map(([k, v]) => `${k}="${v}"`)
                    .join(' ');
                result += `<${tagName}${attribs ? ' ' + attribs : ''}>{{ ${tCall} }}</${tagName}>`;
            } else {
                result += `{{ ${tCall} }}`;
            }
        } else {
            result += `{{ ${tCall} }}`;
        }
    });

    // Append any leftover tags
    for (; matchedKeyIdx < rootNodes.length; matchedKeyIdx++) {
        const node = rootNodes[matchedKeyIdx];
        if (node.type === 'tag') {
            const tagName = node.name;
            const attribs = Object.entries(node.attribs || {})
            .map(([k, v]) => `${k}="${v}"`)
            .join(' ');
            result += `<${tagName}${attribs ? ' ' + attribs : ''}></${tagName}>`;
        }
    }
    // ✅ Wrap in the original opening and closing tag
    return `<${tagName}${attribs ? ' ' + attribs : ''}>${result}</${tagName}>`;
}

module.exports = { replace };