Skip to content
This repository has been archived by the owner on Oct 16, 2020. It is now read-only.

Import export #415

Open
wants to merge 2 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ Any type of contributions are welcome.
* json - [.json, .arb (Flutter Internationalization)]
* yaml - [.yaml, .yml]

#### Import / export translations

* Import translations from XLSX [.xlsx, .xls] files
* Export translations to XLSX or Comma-separated [.xlsx, .csv] files

**Feature requests and/or pull requests with new plugins are welcomed 🙂**

**If you want to test the features, you can open the testData folder!**
Expand Down
3 changes: 3 additions & 0 deletions common/ipcMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ export const saveSettings = 'saveSettings';
export const recentFolders = 'recentFolders';
export const closeFolder = 'closeFolder';
export const refreshFolder = 'refreshFolder';
export const showExport = 'showExport';
export const createXls = 'createXls';
export const exportComplete = 'exportComplete';
19 changes: 19 additions & 0 deletions main/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ParsedFile } from '../common/types';
import * as fileManager from './fileManager';
import * as settings from './Settings';
import * as windowManager from './windowManager';
import { getCurrentWindow } from './windowManager'

const onSave = async (e: any, data: any) => {
const window = BrowserWindow.fromWebContents(e.sender);
Expand Down Expand Up @@ -38,6 +39,23 @@ const onSave = async (e: any, data: any) => {
}
};

const createXLS = async (e: any, data: Object) => {
const window = getCurrentWindow();
if (!window) {
return false;
}
try {
const res = await fileManager.saveXls(data, window);
if (res) {
dialog.showMessageBox(window, { message: 'Export complete' });
}
} catch (e) {
dialog.showErrorBox('Failed export', 'Failed to create xls or csv file');
} finally {
windowManager.sendExportComplete(window)
}
}

const onOpen = (e: any, data: string) => {
const window = BrowserWindow.fromWebContents(e.sender);
if (!window) return;
Expand Down Expand Up @@ -81,6 +99,7 @@ const registerAppEvents = () => {
ipcMain.on(ipcMessages.saveSettings, onSaveSettings);
ipcMain.on(ipcMessages.settings, onGetSettings);
ipcMain.on(ipcMessages.recentFolders, onRecentFolders);
ipcMain.on(ipcMessages.createXls, createXLS);

app.on('open-file', onOpenFile);
app.on('will-finish-launching', () => {
Expand Down
95 changes: 90 additions & 5 deletions main/fileManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,62 @@ import { exists } from 'fs';
import { promisify } from 'util';
import nodeWatch from 'node-watch';
import * as _ from 'lodash/fp';
import * as xlsx from 'xlsx';

import { LoadedFolder, LoadedGroup, LoadedPath, ParsedFile } from '../common/types';
import { loadFolder, saveFile } from './pluginManager';
import { loadFolder, saveFile, parseXlsx } from './pluginManager';
import * as settings from './Settings';
import {
createWindow,
getAvailableWindow,
getAvailableWindow, getCurrentWindow,
sendClose,
sendOpen,
sendSave,
sendRecentFolders,
sendRefreshFolder,
} from './windowManager';
sendRefreshFolder
} from './windowManager'

const existsAsync = promisify(exists);
let watcher: any;

export const openFolder = async (folderPath: string) => {
const window = getAvailableWindow() || createWindow();
await openFolderInWindow(folderPath, window);
};

export const openFile = async (filePath: string) => {
const window = getAvailableWindow() || createWindow();
const isValidPath = await existsAsync(filePath);
if (!isValidPath) {
dialog.showMessageBox(window, {
type: 'error',
message: `File not found in the given path "${filePath}"`,
});
} else {
const { canceled, filePaths } = await dialog.showOpenDialog({
title: 'Select save directory',
properties: ['openDirectory'],
});
if (canceled) { return; }
const savePath = filePaths[0];
const parsedData = await parseXlsx(filePath, savePath);
if (parsedData.length) {
await sendOpen(window, savePath, parsedData);
sendSave(window);
watchFolder(window, savePath);

app.addRecentDocument(savePath);
const recentFolders = settings.addRecentFolder(savePath);
sendRecentFolders(window, recentFolders);
} else {
dialog.showMessageBox(window, {
type: 'error',
message: `Data not found in the given file "${filePath}"`,
});
}
}
};

export const openFolderInWindow = async (folderPath: string, window: Electron.BrowserWindow) => {
let recentFolders: string[];

Expand Down Expand Up @@ -60,18 +96,67 @@ export const saveFolder = async (data: LoadedPath[]): Promise<string[]> => {
);
};

export const saveXls = async (data: any, window: Electron.BrowserWindow) => {
if (!window) {
return false
}
const file = await dialog.showSaveDialog(window, {
filters: [
{ name: 'Microsoft Excel (xlsx)', extensions: ['xlsx', 'xls'] },
{ name: 'Comma-separated values (csv)', extensions: ['csv'] },
],
});
if (file.canceled) {
return false;
}
const { selectedLanguages, sheetData } = data;
const wb = xlsx.utils.book_new();
const ws = xlsx.utils.aoa_to_sheet([['filename', 'label', ...selectedLanguages]]);
for (const fileName in sheetData) {
for (const label in sheetData[fileName]) {
let row = [fileName, label];
selectedLanguages.map((lang: string) => {
const val = sheetData[fileName][label][lang] || '';
row.push(val);
});
xlsx.utils.sheet_add_aoa(ws, [
[...row]
], {
origin: -1,
});
}
}
const filePath = file.filePath || ''
xlsx.utils.book_append_sheet(wb, ws);
try {
await xlsx.writeFile(wb, filePath);
return true;
} catch (e) {
throw e;
}
};

const getParsedFiles = (data: LoadedPath[]): ParsedFile[] =>
data
.map((it) =>
it.type === 'file' ? (it as LoadedGroup).items : getParsedFiles((it as LoadedFolder).items),
)
.flat();

export const closeFolderWatcher = function () {
if (typeof watcher !== undefined) {
if (watcher && typeof watcher.close !== undefined) {
watcher.close();
}
}
}

const watchFolder = (window: Electron.BrowserWindow, folderPath: string) => {
const handleFileUpdate = _.debounce(1000, async () => {
const parsedFiles = await loadFolder(folderPath);
sendRefreshFolder(window, parsedFiles);
});

nodeWatch(folderPath, { recursive: true }, handleFileUpdate);
closeFolderWatcher();
watcher = nodeWatch(folderPath, { recursive: true }, handleFileUpdate);
};
41 changes: 41 additions & 0 deletions main/menu/import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { dialog } from 'electron';
import { openFile } from '../fileManager';
import * as windowManager from '../windowManager';

const openDirectory = async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
filters: [
{ name: 'Supported files xls / xlsx', extensions: ['xls', 'xlsx'] },
{ name: 'All Files', extensions: ['*'] },
],
properties: ['openFile'],
});
if (!canceled) {
openFile(filePaths[0]);
}
};

const openExport = async () => {
const window = windowManager.getCurrentWindow();
if (!window) {
return;
}

windowManager.sendShowExport(window);
};

const importMenu: Electron.MenuItemConstructorOptions = {
label: 'Import / Export',
submenu: [
{
label: 'Import From XLSX',
click: openDirectory,
},
{
label: 'Export To XLSX or CSV',
click: openExport,
},
],
};

export default importMenu;
2 changes: 2 additions & 0 deletions main/menu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import fileMenu from './file';
import helpMenu from './help';
import viewMenu from './view';
import windowMenu from './window';
import importMenu from './import'


import MenuItem = Electron.MenuItem;
Expand All @@ -18,6 +19,7 @@ if (Object.keys(appMenu).length > 0) {
}
menuTemplate.push(fileMenu);
menuTemplate.push(editMenu);
menuTemplate.push(importMenu);
menuTemplate.push(viewMenu);
menuTemplate.push(windowMenu);
menuTemplate.push(helpMenu);
Expand Down
56 changes: 55 additions & 1 deletion main/pluginManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import * as fs from 'fs';
import * as _ from 'lodash';
import * as path from 'path';
import * as util from 'util';
import * as xlsx from 'xlsx';

import { getLocale } from '../common/language';
import { LoadedFolder, LoadedGroup, LoadedPath, ParsedFile } from '../common/types';
import getPlugins, { IPlugin } from './plugins';
import { setWith, omit } from 'lodash';

const readdirAsync = util.promisify(fs.readdir);
const readFileAsync = util.promisify(fs.readFile);
Expand All @@ -27,6 +29,59 @@ export const loadFolder = async (folderPath: string): Promise<LoadedPath[]> => {
return groupedFiles.concat(groupedLanguageFolders).concat(subFolders);
};

export const parseXlsx = async (filePath: string, savePath: string): Promise<LoadedGroup[]> => {
try {
const workbook = await xlsx.readFile(filePath, {
cellHTML: false,
});
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const sheetJson = xlsx.utils.sheet_to_json(sheet);
const sheetObject: { [key: string]: { [key: string]: ParsedFile } } = {};
await sheetJson.map((row: any) => {
let filename: any = row.filename;
let label: any = row.label;
const languages: any = omit(row, ['filename', 'label']);
for (const lang in languages) {
if (!sheetObject[filename]) {
sheetObject[filename] = {};
}
if (!sheetObject[filename][lang]) {
sheetObject[filename][lang] = {
fileName: filename + '_' + lang,
filePath: savePath + '\\' + filename + '_' + lang + '.json',
prefix: filename,
language: lang,
extension: '.json',
data: {},
} as ParsedFile;

try {
writeFileAsync(savePath + '\\' + filename + '_' + lang + '.json', '{}');
} catch (e) {}

}
setWith(sheetObject[filename][lang].data, label, languages[lang], Object);
}
});
const parsedFiles: LoadedGroup[] = [];
const files = Object.keys(sheetObject);
await files.map((filename) => {
const items: ParsedFile[] = [];
for (const k in sheetObject[filename]) {
items.push(sheetObject[filename][k]);
}
parsedFiles.push({
type: 'file',
name: filename,
items: items,
} as LoadedGroup);
});
return parsedFiles;
} catch (e) {
return [];
}
};

export const parseFile = async (filePath: string): Promise<any> => {
try {
const fileContent = await readFileAsync(filePath);
Expand All @@ -53,7 +108,6 @@ export const saveFile = async (parsedFile: ParsedFile): Promise<boolean> => {
const data = await plugin.parse(fileContent.toString());
const updatedData = mergeDrop(data, parsedFile.data);


const serializedContent = await plugin.serialize(updatedData);
if (serializedContent === null) {
return false;
Expand Down
2 changes: 1 addition & 1 deletion main/plugins/json/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const parse = (content: string): Promise<any> => {

export const serialize = async (data: object): Promise<string | undefined> => {
try {
return JSON.stringify(data, Object.keys(data).sort(), 2);
return JSON.stringify(data, undefined, 2);
} catch (e) {
return undefined;
}
Expand Down
10 changes: 10 additions & 0 deletions main/windowManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as ipcMessages from '../common/ipcMessages';
import { LoadedPath } from '../common/types';
import { getFormattedFoldersPaths } from './pathUtils';
import * as settings from './Settings';
import { closeFolderWatcher } from './fileManager'

export const hasWindows = (): boolean => BrowserWindow.getAllWindows().length > 0;

Expand Down Expand Up @@ -73,6 +74,7 @@ export const sendRecentFolders = (window: BrowserWindow, data: string[]) => {
};

export const sendClose = (window: BrowserWindow) => {
closeFolderWatcher();
sendToIpc(window, ipcMessages.closeFolder, {});
};

Expand All @@ -86,6 +88,14 @@ const sendToIpc = (window: BrowserWindow, message: string, data?: any) => {
}
};

export const sendShowExport = (window: BrowserWindow) => {
sendToIpc(window, ipcMessages.showExport);
};

export const sendExportComplete = (window: BrowserWindow) => {
sendToIpc(window, ipcMessages.exportComplete)
};

export enum SaveResponse {
Save,
Cancel,
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"node-watch": "^0.6.3",
"rimraf": "^3.0.2",
"roboto-fontface": "*",
"snyk": "^1.316.1",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-router": "^3.1.6",
Expand All @@ -81,7 +82,7 @@
"vuex": "^3.1.3",
"vuex-class": "^0.3.2",
"vuex-module-decorators": "^0.16.1",
"snyk": "^1.316.1"
"xlsx": "^0.16.7"
},
"browserslist": [
"last 2 Chrome versions"
Expand Down
Loading