@o3r/localization
Version:
This module provides a runtime dynamic language/translation support and debug tools.
301 lines • 12.8 kB
JavaScript
;
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