resx-json-typescript-converter
Version:
Converts resx files to json and optionaly generates a TypeScript resourceManager.
474 lines (356 loc) • 14.4 kB
text/typescript
declare const __dirname: string;
declare const require: any;
import * as fs from 'fs';
import * as path from 'path';
import fileseek from 'fileseek_plus';
import { Parser as XmlParser } from 'xml2js';
export interface res2TsOptions {
mergeCulturesToSingleFile: boolean;
generateTypeScriptResourceManager: boolean;
searchRecursive: boolean;
defaultResxCulture: string;
}
class Options implements res2TsOptions {
public mergeCulturesToSingleFile: boolean = true;
public generateTypeScriptResourceManager: boolean = true;
public searchRecursive: boolean = false;
public defaultResxCulture: string = 'en';
constructor(optionsObject: res2TsOptions) {
if(optionsObject == null) {
return;
}
if(Object.hasOwnProperty.call(optionsObject, 'mergeCulturesToSingleFile') && typeof optionsObject.mergeCulturesToSingleFile == 'boolean') {
this.mergeCulturesToSingleFile = optionsObject.mergeCulturesToSingleFile;
}
if(Object.hasOwnProperty.call(optionsObject, 'generateTypeScriptResourceManager') && typeof optionsObject.generateTypeScriptResourceManager == 'boolean') {
this.generateTypeScriptResourceManager = optionsObject.generateTypeScriptResourceManager;
}
if(Object.hasOwnProperty.call(optionsObject, 'searchRecursive') && typeof optionsObject.searchRecursive == 'boolean') {
this.searchRecursive = optionsObject.searchRecursive;
}
if(Object.hasOwnProperty.call(optionsObject, 'defaultResxCulture') && typeof optionsObject.defaultResxCulture == 'string') {
this.defaultResxCulture = optionsObject.defaultResxCulture;
}
}
}
export function convertResx(resxInput: string | string[], outputFolder: string, options: res2TsOptions = null): void {
// Read and validate the users options
let OptionsInternal = new Options(options);
// Check if an Input-Path was given
if (resxInput === undefined || resxInput === '') {
// files = search.recursiveSearchSync(/.resx$/, __dirname + virtualProjectRoot );
console.error('No input-path given');
return;
}
// Normalize the output path
outputFolder = path.normalize(outputFolder);
// Get the resx-file(s) from the input path
let files: string[] = [];
files = findFiles(resxInput, OptionsInternal.searchRecursive)
// Check wether there are some files in the Input path
if(files.length < 1) {
console.log('No *.resx-files found in the input path.');
return;
}
// Sort the files for their base resource and their culture
let filesSorted = sortFilesByRes(files, OptionsInternal.defaultResxCulture);
// Generate the JSON from the files (and get a list of all keys for the resource-manager generation)
let resourceNameList = generateJson(filesSorted, outputFolder, OptionsInternal.mergeCulturesToSingleFile)
// Generate the resource-manager (if set in the options)
if(OptionsInternal.generateTypeScriptResourceManager) {
generateResourceManager(outputFolder, resourceNameList, OptionsInternal.mergeCulturesToSingleFile, OptionsInternal.defaultResxCulture);
}
return;
}
let parser: XmlParser;
function findFiles(resxInput: string | string[], recursiveSearch: boolean): string[] {
if(resxInput == null) {
console.error('No input filepath given');
return [];
}
if(typeof resxInput == 'string') {
return getFilesForPath(resxInput, recursiveSearch);
}
if(!Array.isArray(resxInput)) {
console.warn('The given input path is neither an string[] nor a single string');
return [];
}
let files: string [] = [];
for(let inPath of resxInput) {
let filesInPath = getFilesForPath(inPath, recursiveSearch);
for(let file of filesInPath) {
if(!files.includes(file)) {
files.push(file);
}
}
}
return files;
}
function getFilesForPath(inputPath: string, recursiveSearch: boolean): string[]{
let files: string [] = [];
if(inputPath.endsWith('.resx') ) {
if(!fs.existsSync(inputPath)) {
console.warn(`The file or path '${inputPath}' could not be found.`);
return files;
}
files.push(inputPath);
return files;
}
//TODO wait for the fileseek maintainer to merge my pull request
files = fileseek(inputPath, /.resx$/, recursiveSearch);
return files;
}
function sortFilesByRes(inputFiles: string [], defaultCulture: string): resxFiles {
let sorted: resxFiles = {}
for (let file of inputFiles)
{
//Filename and Culture
let info = getResxFileInfo(file);
if(info.culture == null) {
info.culture = defaultCulture;
}
if(!Object.hasOwnProperty.call(sorted, info.name)) {
sorted[info.name] = {}
}
sorted[info.name][info.culture] = file;
}
return sorted;
}
function generateJson(resxFiles: resxFiles, outputFolder: string, mergeCultures: boolean): resourceFileKeyCollection {
if(parser == undefined || parser == null) {
parser = new XmlParser()
}
//Create the Directory before we write to it
if(!fs.existsSync(outputFolder)) {
fs.mkdirSync(outputFolder, {recursive: true})
}
let resourceFileKeyCollection: resourceFileKeyCollection = {};
for (const resxFileName in resxFiles) {
let cultureFiles = resxFiles[resxFileName];
let resourceKeys: resxFileKeys;
if(mergeCultures) {
resourceKeys = generateJsonMerged(outputFolder, cultureFiles, resxFileName);
} else {
resourceKeys = generateJsonSingle(outputFolder, cultureFiles, resxFileName);
}
resourceFileKeyCollection[resxFileName] = resourceKeys;
}
return resourceFileKeyCollection;
}
function generateJsonMerged(outputFolder: string, cultureFiles: resxFileCulture, resourceName: string): resxFileKeys {
let resKeys: string[] = [];
let o: {[key: string]: resxKeyValues} = {};
for (let culture in cultureFiles)
{
let file = cultureFiles[culture];
let resxContentObject = getResxKeyValues(file);
o[culture] = resxContentObject;
// Add the ResourceKeys to the key collection
for(let key of Object.keys(resxContentObject)){
if(!resKeys.includes(key)) {
resKeys.push(key);
}
}
}
//Json stringify
let content: string = JSON.stringify(o);
//Write the file
let targetFileName = `${resourceName}.json`;
let targetPath = path.join(outputFolder, targetFileName);
targetPath = path.normalize(targetPath);
fs.writeFileSync(targetPath, content, {encoding: 'utf-8'});
return {
resourcename: resourceName,
generatedFiles: [targetFileName],
resxKeys: resKeys
}
}
function generateJsonSingle(outputFolder: string, cultureFiles: resxFileCulture, resourceName: string): resxFileKeys {
let resKeys: string[] = [];
let targetFiles: string[] = [];
for (let culture in cultureFiles)
{
let file = cultureFiles[culture];
let resxContentObject = getResxKeyValues(file);
let o: {[key: string]: resxKeyValues} = {};
o[culture] = resxContentObject;
//Json strinify
let content: string = JSON.stringify(o);
//Write the file
let targetFileName = `${resourceName}.${culture}.json`;
let targetPath = path.join(outputFolder, targetFileName);
targetPath = path.normalize(targetPath);
fs.writeFileSync(targetPath, content, {encoding: 'utf-8'});
targetFiles.push(targetFileName);
// Add the ResourceKeys to the key collection
for(let key of Object.keys(resxContentObject)){
if(!resKeys.includes(key)) {
resKeys.push(key);
}
}
}
return {
resourcename: resourceName,
generatedFiles: targetFiles,
resxKeys: resKeys
}
}
function generateResourceManager(outputFolder: string, resourceNameList: resourceFileKeyCollection, isResourcesMergedByCulture: boolean, defaultCulture: string) {
let classesString: string = '';
let classInstancesString: string ='';
for (let resourceInfo of Object.values(resourceNameList)) {
let resourceName = resourceInfo.resourcename;
classInstancesString += `
private _${resourceName}: ${resourceName} = new ${resourceName}(this);
get ${resourceName}(): ${resourceName} {
return this._${resourceName};
}
`;
let resourceGetters: string = '';
for (let resxIdentifier of resourceInfo.resxKeys){
resourceGetters += `
get ${resxIdentifier}(): string {
return this.get('${resxIdentifier}');
}
`;
}
if(isResourcesMergedByCulture) {
classesString += `
import * as resx${resourceName} from './${resourceInfo.generatedFiles[0].trim()}';
export class ${resourceName} extends resourceFile {
constructor(resourceManager: resourceManager) {
super(resourceManager);
this.resources = (<any>resx${resourceName}).default;
}
${resourceGetters}
}
`;
} else {
let importStatements: string = '';
let importNames: string[] = [];
let resourceConstruction: string = '';
for(let filename of resourceInfo.generatedFiles) {
let importname = '' + filename;
importname.replace('.', '_');
importNames.push(importname);
importStatements += `
import * as ${importname} from './${filename}'`;
}
resourceConstruction = importNames.join(', ');
classesString = `
${importStatements}
export class P3JS_1 extends resourceFile {
constructor(resourceManager: resourceManager) {
super(resourceManager);
this.resources = Object.assign(${resourceConstruction});
}
${resourceGetters}
}
`;
}
}
let resxManagerString = `
/**
* This class gives you type-hinting for the automatic generated resx-json files
*/
export default class resourceManager {
public language: string;
constructor(language: string) {
this.language = language;
}
public setLanguage(language: string) {
this.language = language;
};
// Generated class instances start
${classInstancesString}
// Gen end
}
abstract class resourceFile {
protected resMan: resourceManager;
protected resources: { [langKey: string]: { [resKey: string]: string } } = {};
constructor(resourceManager: resourceManager) {
this.resMan = resourceManager;
}
public get(resKey: string) {
let language = this.resMan.language;
// Check if the language exists for this resource and if the language has an corresponsing key
if (Object.hasOwnProperty.call(this.resources, language) && Object.hasOwnProperty.call(this.resources[language], resKey)) {
return this.resources[language][resKey];
}
// If no entry could be found in the currently active language, try the default language
if (Object.hasOwnProperty.call(this.resources, '${defaultCulture}') && Object.hasOwnProperty.call(this.resources['${defaultCulture}'], resKey)) {
console.log(\`No text resource in the language "\${language}" with the key "\${resKey}".\`);
return this.resources['${defaultCulture}'][resKey];
}
// If there is still no resource found output a warning and return the key.
console.warn(\`No text-resource for the key \${resKey} found.\`);
return resKey;
};
}
// Gen Classes start
${classesString}
// Gen Classes end
`;
//Write the file
let targetFileName = `resourceManager.ts`;
let targetPath = path.join(outputFolder, targetFileName);
targetPath = path.normalize(targetPath);
fs.writeFileSync(targetPath, resxManagerString, {encoding: 'utf-8'});
}
function getResxFileInfo(filePath: string): resxFileInfo {
let fileCulture: string = null;
let nameClean: string;
let filename: string = path.basename(filePath);
let filenameSplit = filename.split('.');
filenameSplit.pop();
if(filenameSplit.length > 1) {
fileCulture = filenameSplit.pop();
}
nameClean = filenameSplit.join('.');
return {
name: nameClean,
culture: fileCulture
}
}
function getResxKeyValues(filepath: string): resxKeyValues {
const resources: resxKeyValues = {};
parser.reset();
let fileContentString = fs.readFileSync(filepath, {encoding: 'utf-8'})
parser.parseString(fileContentString, function (err: any, xmlObject: any) {
if(xmlObject == undefined ||
xmlObject == null ||
!Object.hasOwnProperty.call(xmlObject, 'root') ||
!Object.hasOwnProperty.call(xmlObject.root, 'data') ||
xmlObject.root.data == undefined) {
return;
}
for (let i in xmlObject.root.data)
{
const name = xmlObject.root.data[i].$.name;
const value = xmlObject.root.data[i].value.toString();
resources[name] = value;
}
});
return resources;
}
interface resxFileInfo {
name: string;
culture: string;
}
interface resxFiles {
[key: string]: resxFileCulture;
}
interface resxFileCulture {
[key: string]: string;
}
interface resxKeyValues {
[key: string]: string;
}
interface resxFileKeys {
resourcename: string;
generatedFiles: string[];
resxKeys: string[];
}
interface resourceFileKeyCollection {
[resourceFileName: string]: resxFileKeys
}