UNPKG

sfdx-hardis

Version:

Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards

311 lines • 14.7 kB
import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import c from "chalk"; import * as path from "path"; import fs from "fs-extra"; import { Messages } from '@salesforce/core'; import { isCI, uxLog } from '../../../common/utils/index.js'; import { prompts } from '../../../common/utils/prompts.js'; import { WebSocketClient } from '../../../common/websocketClient.js'; import { parseStringPromise, Builder } from 'xml2js'; import { GLOB_IGNORE_PATTERNS } from '../../../common/utils/projectUtils.js'; import { glob } from 'glob'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); export default class CustomLabelTranslations extends SfCommand { static title = 'Custom Label Translations'; static description = `Extract selected custom labels, or of a given Lightning Web Component (LWC), from all language translation files. This command generates translation files ('*.translation - meta.xml') for each language already retrieved in the current project, containing only the specified custom labels.`; static examples = [ '$ sf hardis:misc:custom-label-translations --label CustomLabelName', '$ sf hardis:misc:custom-label-translations --label Label1,Label2', '$ sf hardis:misc:custom-label-translations --lwc MyComponent' ]; outputDirPrefix = 'extract-'; static flags = { label: Flags.string({ char: 'l', description: 'Developer name(s) of the custom label(s), comma-separated', }), lwc: Flags.string({ char: 'c', description: 'Developer name of the Lightning Web Component', }), debug: Flags.boolean({ char: 'd', default: false, description: messages.getMessage('debugMode'), }), websocket: Flags.string({ description: messages.getMessage('websocket'), }), skipauth: Flags.boolean({ description: 'Skip authentication check when a default username is required', }), }; // Set this to true if your command requires a project workspace; 'requiresProject' is false by default static requiresProject = true; /** * Extract custom label names from LWC JS files */ async extractLabelsFromLwc(lwcName, debugMode) { uxLog(this, c.grey(`Looking for LWC '${lwcName}' JS files...`)); const lwcFiles = await glob(`**/lwc/${lwcName}/**/*.js`); if (lwcFiles.length === 0) { throw new Error(`No JS files found for LWC '${lwcName}'`); } uxLog(this, c.grey(`Found ${lwcFiles.length} JS files for component '${lwcName}'`)); const labelNames = new Set(); const labelImportRegex = /@salesforce\/label\/c\.([a-zA-Z0-9_]+)/g; for (const jsFile of lwcFiles) { const content = await fs.readFile(jsFile, 'utf8'); let match; while ((match = labelImportRegex.exec(content)) !== null) { labelNames.add(match[1]); } if (debugMode) { uxLog(this, c.grey(`Processed file: ${jsFile}`)); } } const extractedLabels = Array.from(labelNames); if (extractedLabels.length === 0) { throw new Error(`No custom labels found in LWC '${lwcName}'`); } uxLog(this, c.grey(`Found ${extractedLabels.length} custom labels in LWC '${lwcName}': ${extractedLabels.join(', ')}`)); this.outputDirPrefix = lwcName; return extractedLabels; } async run() { const { flags } = await this.parse(CustomLabelTranslations); const debugMode = flags.debug || false; let labelNames = []; if (flags.lwc) { try { labelNames = await this.extractLabelsFromLwc(flags.lwc, debugMode); } catch (error) { uxLog(this, c.red(error.message)); return { success: false, message: error.message }; } } else if (flags.label) { labelNames = flags.label.split(',').map(label => label.trim()); } else if (!isCI) { const selection = await CustomLabelTranslations.promptExtractionMethod(); if (selection.type == 'labels') { labelNames = selection.values; } else if (selection.type == 'lwc') { labelNames = await this.extractLabelsFromLwc(selection.values, debugMode); } } if (!labelNames || labelNames.length === 0) { const errorMsg = 'No custom labels specified. Use --label or --lwc flag.'; uxLog(this, c.red(errorMsg)); return { success: false, message: errorMsg }; } uxLog(this, c.grey(`Processing custom labels: ${labelNames.join(', ')}`)); try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const outputDir = path.join('extracted-translations', `${this.outputDirPrefix}-${timestamp}`); await fs.ensureDir(outputDir); const translationFiles = await glob('**/translations/*.translation-meta.xml'); if (translationFiles.length === 0) { uxLog(this, c.yellow(`No translation files found in **/translations/`)); return { success: false, message: 'No translation files found' }; } const results = {}; for (const translationFile of translationFiles) { const languageCode = path.basename(translationFile).replace('.translation-meta.xml', ''); uxLog(this, c.grey(`Processing translation file for ${languageCode}...`)); const xmlContent = await fs.readFile(translationFile, 'utf8'); const parsedXml = await parseStringPromise(xmlContent, { explicitArray: false }); if (!parsedXml.Translations) { uxLog(this, c.yellow(`Invalid translation file format: ${translationFile}`)); continue; } if (!parsedXml.Translations.customLabels) { uxLog(this, c.yellow(`No custom labels found in ${translationFile}`)); continue; } const customLabels = Array.isArray(parsedXml.Translations.customLabels) ? parsedXml.Translations.customLabels : [parsedXml.Translations.customLabels]; const matchedLabels = customLabels.filter(label => labelNames.includes(label.name)); if (matchedLabels.length === 0) { uxLog(this, c.yellow(`No matching custom labels found in ${languageCode}`)); continue; } const newXml = { Translations: { $: { xmlns: "http://soap.sforce.com/2006/04/metadata" }, customLabels: matchedLabels } }; const builder = new Builder({ xmldec: { version: '1.0', encoding: 'UTF-8' }, renderOpts: { pretty: true, indent: ' ', newline: '\n' } }); const outputXml = builder.buildObject(newXml); const outputFile = path.join(outputDir, `${languageCode}.translation-meta.xml`); await fs.writeFile(outputFile, outputXml); results[languageCode] = { file: outputFile, matchedLabels: matchedLabels.length }; if (debugMode) { uxLog(this, c.grey(`Found ${matchedLabels.length} labels in ${languageCode}:`)); matchedLabels.forEach(label => { uxLog(this, c.grey(` ${label.name} = "${label.label}"`)); }); } } const totalFiles = Object.keys(results).length; if (totalFiles === 0) { uxLog(this, c.yellow('No matching labels found in any translation file.')); return { success: false, message: 'No matching labels found' }; } uxLog(this, c.green(`Successfully extracted custom labels to ${outputDir}`)); uxLog(this, c.grey(`Processed ${totalFiles} translation files`)); WebSocketClient.requestOpenFile(outputDir); // Return an object to be displayed with --json return { success: true, outputDirectory: outputDir, results: results }; } catch (err) { uxLog(this, c.red(`Error processing custom labels: ${err.message}`)); throw err; } } static async promptCustomLabels() { try { const customLabelsFiles = await glob('**/labels/CustomLabels.labels-meta.xml', { ignore: GLOB_IGNORE_PATTERNS }); if (customLabelsFiles.length == 0) { throw new Error('No CustomLabels.labels-meta.xml was found'); } const choices = []; for (const customLabelsFile of customLabelsFiles) { const xmlContent = await fs.readFile(customLabelsFile, 'utf8'); const parsedXml = await parseStringPromise(xmlContent); if (!parsedXml.CustomLabels || !parsedXml.CustomLabels.labels) { throw new Error('No custom labels found in the file'); } const labels = Array.isArray(parsedXml.CustomLabels.labels) ? parsedXml.CustomLabels.labels : [parsedXml.CustomLabels.labels]; labels.sort((a, b) => { const nameA = a.fullName ? a.fullName[0] : a.name ? a.name[0] : ''; const nameB = b.fullName ? b.fullName[0] : b.name ? b.name[0] : ''; return nameA.localeCompare(nameB, 'en', { sensitivity: 'base' }); }); labels.map(label => { const name = label.fullName ? label.fullName[0] : label.name ? label.name[0] : ''; const value = label.value ? label.value[0] : ''; const shortDesc = value.length > 40 ? value.substring(0, 40) + '...' : value; choices.push({ value: name, title: name, description: shortDesc }); }); } const labelSelectRes = await prompts({ type: 'multiselect', message: 'Please select the Custom Labels you want to extract from translations', choices: choices }); return labelSelectRes.value; } catch (err) { console.error('Error while processing custom labels:', err.message); throw err; } } static async promptLwcComponent() { try { const lwcMetaFiles = await glob('**/lwc/*/*.js-meta.xml'); if (lwcMetaFiles.length === 0) { throw new Error('No Lightning Web Components found in the project'); } const componentsInfo = []; for (const metaFile of lwcMetaFiles) { try { const xmlContent = await fs.readFile(metaFile, 'utf8'); const parsedXml = await parseStringPromise(xmlContent); const pathParts = metaFile.split('/'); const componentName = pathParts[pathParts.length - 1].replace('.js-meta.xml', ''); let masterLabel = componentName; if (parsedXml.LightningComponentBundle && parsedXml.LightningComponentBundle.masterLabel && parsedXml.LightningComponentBundle.masterLabel.length > 0) { masterLabel = parsedXml.LightningComponentBundle.masterLabel[0]; } componentsInfo.push({ name: componentName, label: masterLabel, path: metaFile }); } catch (err) { console.warn(`Could not parse meta file: ${metaFile}`, err.message); } } componentsInfo.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' })); const choices = componentsInfo.map(component => ({ value: component.name, title: component.label, description: `Name: ${component.name}` })); const componentSelectRes = await prompts({ type: 'select', name: 'value', message: 'Select a Lightning Web Component to extract custom labels from', choices: choices }); return componentSelectRes.value; } catch (err) { console.error('Error while finding LWC components:', err.message); throw err; } } static async promptExtractionMethod() { try { const methodSelectRes = await prompts({ type: 'select', name: 'method', message: 'How would you like to extract custom label translations?', choices: [ { value: 'labels', title: 'Select specific custom labels', description: 'Choose one or more custom labels from the full list' }, { value: 'lwc', title: 'Extract from a Lightning Web Component', description: 'Find all custom labels used in a specific LWC' } ] }); let values; if (methodSelectRes.method === 'labels') { values = await CustomLabelTranslations.promptCustomLabels(); } else if (methodSelectRes.method === 'lwc') { values = await CustomLabelTranslations.promptLwcComponent(); } return { type: methodSelectRes.method, values: values }; } catch (err) { console.error('Error during extraction method selection:', err.message); throw err; } } } //# sourceMappingURL=custom-label-translations.js.map