UNPKG

@o3r/localization

Version:

This module provides a runtime dynamic language/translation support and debug tools.

301 lines • 12.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LocalizationExtractor = void 0; const fs = require("node:fs"); const path = require("node:path"); const extractors_1 = require("@o3r/extractors"); const schematics_1 = require("@o3r/schematics"); const globby_1 = require("globby"); const ts = require("typescript"); /** List of Angular decorator to look for */ const ANGULAR_ANNOTATION = ['Component', 'Injectable', 'Pipe']; /** * Localization extractor */ class LocalizationExtractor { constructor(tsconfigPath, logger, options) { this.options = options; this.tsconfigPath = tsconfigPath; this.logger = logger; } /** Get the list of file from tsconfig.json */ getFilesFromTsConfig() { const { include, exclude, cwd } = this.getPatternsFromTsConfig(); return (0, globby_1.sync)(include, { ignore: exclude, cwd }); } /** * Return the class node if the class is an angular element * @param source Ts file source */ getAngularClassNode(source) { const angularItems = []; source.forEachChild((item) => { if (!ts.isClassDeclaration(item)) { return; } let isAngularItem = false; item.forEachChild((classItem) => { if (isAngularItem || !ts.isDecorator(classItem)) { return; } const text = classItem.getText(source); const regexp = new RegExp('^@' + ANGULAR_ANNOTATION.map((e) => `(${e})`).join('|')); if (!regexp.test(text)) { return; } isAngularItem = true; }); if (isAngularItem) { angularItems.push(item); } }); return angularItems.length > 0 ? angularItems : undefined; } /** * Get the list of referenced translation files * @param localizationFileContent JSON content of a location file * @param localizationFilePath Path of the localization file */ getReferencedFiles(localizationFileContent, localizationFilePath) { const folder = localizationFilePath ? path.dirname(localizationFilePath) : undefined; const referencedFiles = Object.keys(localizationFileContent) .filter((key) => !!localizationFileContent[key].$ref) .map((key) => ({ key, ref: localizationFileContent[key].$ref.split('#/')[0] })) .filter(({ key, ref }) => { const res = !!ref; if (!res) { this.logger.error(`The reference (${ref}) of the key ${key} is invalid, it will be ignored`); } return res; }) .map(({ ref }) => { if (!ref.startsWith('.')) { if (this.options?.libraries?.length && this.options.libraries.every((lib) => !ref.startsWith(lib))) { try { return require.resolve(ref); } catch { // Should be a local file } } return undefined; } return folder ? path.resolve(folder, ref) : ref; }) .filter((ref) => !!ref); return referencedFiles.length > 0 ? referencedFiles : undefined; } /** * Read a localization file * @param locFile Path to the localization file */ async readLocalizationFile(locFile) { const content = JSON.parse(await fs.promises.readFile(locFile, { encoding: 'utf8' })); if (content.$schema) { delete content.$schema; } return content; } /** * Read a metadata file * @param metadataFile Path to the metadata file */ async readMetadataFile(metadataFile) { return JSON.parse(await fs.promises.readFile(metadataFile, { encoding: 'utf8' })); } /** * Generate a metadata item from a localization item * @param loc Localization item * @param key Key of the localization */ generateMetadataItemFromLocalization(loc, key) { const res = { description: loc.description, dictionary: !!loc.dictionary, referenceData: !!loc.referenceData, key }; if (loc.defaultValue || loc.defaultValue === '') { res.value = loc.defaultValue; } if (loc.tags) { res.tags = loc.tags; } if (loc.$ref) { const [refPath, refKey] = loc.$ref.split('#/', 2); res.ref = refPath.startsWith('.') || this.options?.libraries?.some((lib) => refPath.startsWith(lib)) ? refKey : loc.$ref; } if (typeof loc.defaultValue === 'undefined' && typeof loc.$ref === 'undefined' && !loc.dictionary) { this.logger.error(`${key} has no default value or $ref defined`); throw new schematics_1.O3rCliError(`${key} has no default value or $ref defined`); } return res; } /** * Compares two JSONLocalization object by their keys and returns the result of the string comparison * @param a JSONLocalization * @param b JSONLocalization */ compareKeys(a, b) { const keyA = a.key.toUpperCase(); const keyB = b.key.toUpperCase(); if (keyA < keyB) { return -1; } if (keyA > keyB) { return 1; } return 0; } /** Get the list of patterns from tsconfig.json */ getPatternsFromTsConfig() { const tsconfigResult = ts.readConfigFile(this.tsconfigPath, (p) => ts.sys.readFile(p)); if (tsconfigResult.error) { const stringError = typeof tsconfigResult.error.messageText === 'string' ? tsconfigResult.error.messageText : tsconfigResult.error.messageText.messageText; this.logger.error(stringError); throw new schematics_1.O3rCliError(stringError); } const include = [...(tsconfigResult.config.files || []), ...(tsconfigResult.config.include || [])]; const exclude = tsconfigResult.config.exclude || []; const cwd = path.resolve(path.dirname(this.tsconfigPath), tsconfigResult.config.rootDir || '.'); return { include, exclude, cwd }; } /** * Generate the localization mapping for a list of files * @param localizationFiles Localization files to load * @param alreadyLoadedFiles List of localization files already loadded * @param isDependency Determine if the list of files are dependencies of others */ async getLocalizationMap(localizationFiles, alreadyLoadedFiles = [], isDependency = false) { const mapLocalization = {}; for (const file of localizationFiles) { mapLocalization[file] = { data: await this.readLocalizationFile(file), isDependency }; } const references = localizationFiles .map((file) => this.getReferencedFiles(mapLocalization[file].data, file)) .filter((refs) => !!refs) .reduce((acc, refs) => { acc.push(...refs.filter((ref) => !localizationFiles.includes(ref) && !alreadyLoadedFiles.includes(ref))); return acc; }, []); if (references.length > 0) { return { ...mapLocalization, ...await this.getLocalizationMap(references, [...localizationFiles, ...alreadyLoadedFiles], true) }; } return mapLocalization; } /** * Extract the localization mapping from a tsconfig file * @param extraLocalizationFiles Additional translations to add */ async extractLocalizationFromTsConfig(extraLocalizationFiles = []) { const files = this.getFilesFromTsConfig(); const tsFiles = files .filter((file) => /\.ts$/.test(file)) .map((file) => path.join(path.dirname(this.tsconfigPath), file)); const program = ts.createProgram(tsFiles, {}); const localizationFiles = tsFiles .map((file) => ({ file, source: program.getSourceFile(file) })) .map(({ file, source }) => ({ file, classes: source && this.getAngularClassNode(source), source })) .filter(({ classes }) => !!classes) .map(({ file, classes }) => classes .map((classItem) => (0, extractors_1.getLocalizationFileFromAngularElement)(classItem)) .filter((locFiles) => !!locFiles) .reduce((acc, locFiles) => { acc.push(...locFiles.filter((f) => !acc.includes(f))); return acc; }, []) .map((locFile) => path.resolve(path.dirname(file), locFile))) .reduce((acc, locFiles) => { acc.push(...locFiles.filter((f) => !acc.includes(f))); return acc; }, []); localizationFiles.push(...extraLocalizationFiles.filter((file) => !localizationFiles.includes(file))); return this.getLocalizationMap(localizationFiles); } /** * Retrieve metadata from libraries * @param libraries Libraries on which the project depend */ async getMetadataFromLibraries(libraries) { const metadataFiles = libraries .map((lib) => (0, extractors_1.getLibraryCmsMetadata)(lib).localizationFilePath) .filter((localizationFilePath) => !!localizationFilePath); return this.getMetadataFromFiles(metadataFiles); } /** * Retrieve metadata from metadata files * @param metadataFiles Metadata files */ async getMetadataFromFiles(metadataFiles) { const metadataMap = {}; for (const file of metadataFiles) { metadataMap[file] = await this.readMetadataFile(file); } return metadataMap; } /** * Generate metadata from localization and library metadata mappings * @param localizationMap Map of localization files * @param options Option of generation * @param options.ignoreDuplicateKeys * @param options.libraryMetadata * @param options.outputFile * @param options.sortKeys */ generateMetadata(localizationMap, options) { const metadata = {}; let hasDuplicateKey = false; const addMetadata = (data, origin, libraries = []) => { if (metadata[data.key]) { hasDuplicateKey = true; if (options.ignoreDuplicateKeys) { this.logger.warn(`The key ${data.key} from ${origin} will override the previous value (${metadata[data.key].value || 'ref ' + metadata[data.key].ref} -> ${data.value || 'ref ' + data.ref})`); } else { this.logger.error(`The key ${data.key} from ${origin} try to override the previous value (${metadata[data.key].value || 'ref ' + metadata[data.key].ref} -> ${data.value || 'ref ' + data.ref})`); } } metadata[data.key] = data.ref && data.ref.includes('#/') && libraries.some((lib) => data.ref.startsWith(lib)) ? { ...data, ref: data.ref.split('#/')[1] } : data; }; Object.keys(options.libraryMetadata) .forEach((lib) => options.libraryMetadata[lib].forEach((dataLoc) => addMetadata(dataLoc, lib, this.options?.libraries))); Object.keys(localizationMap) .forEach((locFile) => Object.keys(localizationMap[locFile].data) .forEach((locKey) => addMetadata(this.generateMetadataItemFromLocalization(localizationMap[locFile].data[locKey], locKey), locFile))); if (hasDuplicateKey && !options.ignoreDuplicateKeys) { throw new schematics_1.O3rCliError('Duplicate key'); } const localizationMetadata = Object.values(metadata); let hasUnknownRef = false; localizationMetadata.forEach((data) => { if (data.ref && !data.ref.includes('#/')) { if (!metadata[data.ref]) { hasUnknownRef = true; this.logger.error(`The key ${data.ref} is unknown but referenced by ${data.key}.`); } else if (typeof data.description === 'undefined') { data.description = metadata[data.ref].description; } } }); if (hasUnknownRef) { throw new schematics_1.O3rCliError('Unknown referenced key'); } return options.sortKeys ? localizationMetadata.sort((a, b) => this.compareKeys(a, b)) : localizationMetadata; } } exports.LocalizationExtractor = LocalizationExtractor; //# sourceMappingURL=localization.generator.js.map