mirror of
https://github.com/frappe/books.git
synced 2024-11-09 23:30:56 +00:00
Merge pull request #341 from 18alantom/use-translations
Setup Translations - Part 2 Use Translations
This commit is contained in:
commit
03c7b768e6
@ -7,6 +7,8 @@ const {
|
||||
DEFAULT_DISPLAY_PRECISION,
|
||||
} = require('./utils/consts');
|
||||
const { markRaw } = require('vue');
|
||||
const { ipcRenderer } = require('electron');
|
||||
const { IPC_ACTIONS } = require('@/messages');
|
||||
|
||||
module.exports = {
|
||||
initializeAndRegister(customModels = {}, force = false) {
|
||||
|
@ -93,8 +93,8 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
quickEditFields: [
|
||||
'dateFormat',
|
||||
'locale',
|
||||
'dateFormat',
|
||||
'displayPrecision',
|
||||
'hideGetStarted',
|
||||
'autoReportErrors',
|
||||
|
@ -1,3 +1,4 @@
|
||||
export const DEFAULT_INTERNAL_PRECISION = 11;
|
||||
export const DEFAULT_DISPLAY_PRECISION = 2;
|
||||
export const DEFAULT_LOCALE = 'en-IN';
|
||||
export const DEFAULT_LANGUAGE = 'English';
|
@ -1,3 +1,9 @@
|
||||
import {
|
||||
getIndexFormat,
|
||||
getIndexList,
|
||||
getSnippets,
|
||||
getWhitespaceSanitized,
|
||||
} from '../../scripts/helpers';
|
||||
import { ValueError } from '../common/errors';
|
||||
|
||||
class TranslationString {
|
||||
@ -14,19 +20,23 @@ class TranslationString {
|
||||
return this;
|
||||
}
|
||||
|
||||
#translate(segment) {
|
||||
const startSpace = segment.match(/^\s+/)?.[0] ?? '';
|
||||
const endSpace = segment.match(/\s+$/)?.[0] ?? '';
|
||||
segment = segment.replace(/\s+/g, ' ').trim();
|
||||
// TODO: implement translation backend
|
||||
// segment = translate(segment)
|
||||
return startSpace + segment + endSpace;
|
||||
}
|
||||
|
||||
#formatArg(arg) {
|
||||
return arg ?? '';
|
||||
}
|
||||
|
||||
#translate() {
|
||||
let indexFormat = getIndexFormat(this.args[0]);
|
||||
indexFormat = getWhitespaceSanitized(indexFormat);
|
||||
|
||||
const translatedIndexFormat =
|
||||
this.languageMap[indexFormat]?.translation ?? indexFormat;
|
||||
|
||||
this.argList = getIndexList(translatedIndexFormat).map(
|
||||
(i) => this.argList[i]
|
||||
);
|
||||
this.strList = getSnippets(translatedIndexFormat);
|
||||
}
|
||||
|
||||
#stitch() {
|
||||
if (!(this.args[0] instanceof Array)) {
|
||||
throw new ValueError(
|
||||
@ -36,10 +46,15 @@ class TranslationString {
|
||||
);
|
||||
}
|
||||
|
||||
const strList = this.args[0];
|
||||
const argList = this.args.slice(1);
|
||||
return strList
|
||||
.map((s, i) => this.#translate(s) + this.#formatArg(argList[i]))
|
||||
this.strList = this.args[0];
|
||||
this.argList = this.args.slice(1);
|
||||
|
||||
if (this.languageMap) {
|
||||
this.#translate();
|
||||
}
|
||||
|
||||
return this.strList
|
||||
.map((s, i) => s + this.#formatArg(this.argList[i]))
|
||||
.join('')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
@ -65,3 +80,7 @@ export function T(...args) {
|
||||
export function t(...args) {
|
||||
return new TranslationString(...args).s;
|
||||
}
|
||||
|
||||
export function setLanguageMapOnTranslationString(languageMap) {
|
||||
TranslationString.prototype.languageMap = languageMap;
|
||||
}
|
||||
|
@ -26,6 +26,7 @@
|
||||
"knex": "^0.95.12",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^2.0.2",
|
||||
"node-fetch": "2",
|
||||
"pesa": "^1.1.3",
|
||||
"sqlite3": "npm:@vscode/sqlite3@^5.0.7",
|
||||
"vue": "^3.2.30",
|
||||
|
@ -111,10 +111,11 @@ function printHelp() {
|
||||
`\tbe removed.\n` +
|
||||
`\n` +
|
||||
`Parameters:\n` +
|
||||
`\tlanguage_code : An ISO 693-1 code which has 2 characters eg: en\n` +
|
||||
`\tlanguage_code : An ISO 693-1 code or a locale identifier.\n` +
|
||||
`\n` +
|
||||
`Reference:\n` +
|
||||
`\tISO 693-1 codes: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes`
|
||||
`\tISO 693-1 codes: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes\n` +
|
||||
`\tLocale identifier: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#locale_identification_and_negotiation`
|
||||
);
|
||||
}
|
||||
return shouldPrint;
|
||||
@ -125,7 +126,7 @@ function getLanguageCode() {
|
||||
if (i === -1) {
|
||||
return '';
|
||||
}
|
||||
return process.argv[i + 1]?.toLowerCase() ?? '';
|
||||
return process.argv[i + 1] ?? '';
|
||||
}
|
||||
|
||||
function getTranslationFilePath(languageCode) {
|
||||
@ -213,14 +214,6 @@ async function run() {
|
||||
const languageCode = getLanguageCode();
|
||||
|
||||
console.log();
|
||||
if (languageCode.length !== 0 && languageCode.length !== 2) {
|
||||
console.error(
|
||||
`Invalid language code passed: '${languageCode}'.\n` +
|
||||
`Please use an ISO 639-1 language code: ${'https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes'}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileList = await getFileList(root, ignoreList);
|
||||
const contents = await getFileContents(fileList);
|
||||
const tMap = await getAllTStringsMap(contents);
|
||||
|
@ -33,7 +33,7 @@ function getIndexFormat(inp) {
|
||||
|
||||
function getSnippets(string) {
|
||||
let start = 0;
|
||||
snippets = [...string.matchAll(/\${[^}]+}/g)].map((m) => {
|
||||
const snippets = [...string.matchAll(/\${[^}]+}/g)].map((m) => {
|
||||
let end = m.index;
|
||||
let snip = string.slice(start, end);
|
||||
start = end + m[0].length;
|
||||
|
@ -18,6 +18,13 @@
|
||||
@setup-complete="setupComplete"
|
||||
@setup-canceled="setupCanceled"
|
||||
/>
|
||||
<div
|
||||
id="toast-container"
|
||||
class="absolute bottom-0 flex flex-col items-end mb-3 pr-6"
|
||||
style="width: 100%"
|
||||
>
|
||||
<div id="toast-target" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -103,7 +110,7 @@ export default {
|
||||
this.activeScreen = 'SetupWizard';
|
||||
} else {
|
||||
this.activeScreen = 'Desk';
|
||||
checkForUpdates(false);
|
||||
await checkForUpdates(false);
|
||||
}
|
||||
|
||||
if (!resetRoute) {
|
||||
|
@ -16,6 +16,7 @@ import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
|
||||
import { sendError } from './contactMothership';
|
||||
import { getLanguageMap } from './getLanguageMap';
|
||||
import { IPC_ACTIONS, IPC_CHANNELS, IPC_MESSAGES } from './messages';
|
||||
import saveHtmlAsPdf from './saveHtmlAsPdf';
|
||||
|
||||
@ -220,6 +221,18 @@ ipcMain.handle(IPC_ACTIONS.CHECK_FOR_UPDATES, (event, force) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.GET_LANGUAGE_MAP, async (event, code) => {
|
||||
let obj = { languageMap: {}, success: true, message: '' };
|
||||
try {
|
||||
obj.languageMap = await getLanguageMap(code, isDevelopment);
|
||||
} catch (err) {
|
||||
obj.success = false;
|
||||
obj.message = err.message;
|
||||
}
|
||||
|
||||
return obj;
|
||||
});
|
||||
|
||||
/* ------------------------------
|
||||
* Register autoUpdater events lis
|
||||
* ------------------------------*/
|
||||
|
49
src/components/Controls/LanguageSelector.vue
Normal file
49
src/components/Controls/LanguageSelector.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<FormControl
|
||||
:df="languageDf"
|
||||
:value="value"
|
||||
@change="(v) => setLanguageMap(v, dontReload)"
|
||||
:input-class="'focus:outline-none rounded ' + inputClass"
|
||||
/>
|
||||
</template>
|
||||
<script>
|
||||
import config from '@/config';
|
||||
import { languageCodeMap } from '@/languageCodeMap';
|
||||
import { setLanguageMap } from '@/utils';
|
||||
import { DEFAULT_LANGUAGE } from 'frappe/utils/consts';
|
||||
import FormControl from './FormControl';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
setLanguageMap,
|
||||
},
|
||||
props: {
|
||||
inputClass: {
|
||||
type: String,
|
||||
default:
|
||||
'bg-gray-100 active:bg-gray-200 focus:bg-gray-200 px-3 py-2 text-base',
|
||||
},
|
||||
dontReload: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: { FormControl },
|
||||
computed: {
|
||||
value() {
|
||||
return config.get('language') ?? DEFAULT_LANGUAGE;
|
||||
},
|
||||
languageDf() {
|
||||
languageCodeMap;
|
||||
return {
|
||||
fieldname: 'language',
|
||||
label: this.t`Language`,
|
||||
fieldtype: 'Select',
|
||||
options: Object.keys(languageCodeMap),
|
||||
default: config.get('language') ?? DEFAULT_LANGUAGE,
|
||||
description: this.t`Set the display language.`,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -14,7 +14,7 @@
|
||||
:class="inputClasses"
|
||||
>
|
||||
<select
|
||||
class="appearance-none bg-transparent focus:outline-none w-11/12"
|
||||
class="appearance-none bg-transparent focus:outline-none w-11/12 cursor-pointer"
|
||||
:class="{
|
||||
'pointer-events-none': isReadOnly,
|
||||
'text-gray-400': !value,
|
||||
|
148
src/getLanguageMap.js
Normal file
148
src/getLanguageMap.js
Normal file
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Language files are fetched from the frappe/books repo
|
||||
* the language files before storage have a ISO timestamp
|
||||
* prepended to the file.
|
||||
*
|
||||
* This timestamp denotes the commit datetime, update of the file
|
||||
* takes place only if a new update has been pushed.
|
||||
*/
|
||||
|
||||
const fs = require('fs/promises');
|
||||
const path = require('path');
|
||||
const fetch = require('node-fetch').default;
|
||||
const { splitCsvLine } = require('../scripts/helpers');
|
||||
|
||||
const VALENTINES_DAY = 1644796800000;
|
||||
|
||||
async function getLanguageMap(code, isDevelopment = false) {
|
||||
const contents = await getContents(code, isDevelopment);
|
||||
return getMapFromContents(contents);
|
||||
}
|
||||
|
||||
async function getContents(code, isDevelopment) {
|
||||
if (isDevelopment) {
|
||||
const filePath = path.resolve('translations', `${code}.csv`);
|
||||
const contents = await fs.readFile(filePath, { encoding: 'utf-8' });
|
||||
return ['', contents].join('\n');
|
||||
}
|
||||
|
||||
let contents = await getContentsIfExists();
|
||||
if (contents.length === 0) {
|
||||
contents = (await fetchAndStoreFile(code)) ?? contents;
|
||||
} else {
|
||||
contents = (await getUpdatedContent(code, contents)) ?? contents;
|
||||
}
|
||||
|
||||
if (!contents || contents.length === 0) {
|
||||
throwCouldNotFetchFile(code);
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
function getMapFromContents(contents) {
|
||||
contents = contents.split('\n').slice(1);
|
||||
return contents
|
||||
.map(splitCsvLine)
|
||||
.filter((l) => l.length >= 2)
|
||||
.reduce((acc, l) => {
|
||||
const key = l[0].slice(1, -1);
|
||||
const translation = l[1].slice(1, -1);
|
||||
acc[key] = { translation };
|
||||
|
||||
const context = l.slice(2);
|
||||
if (context.length) {
|
||||
acc.context = context;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
async function getContentsIfExists(code) {
|
||||
const filePath = getFilePath(code);
|
||||
try {
|
||||
return await fs.readFile(filePath, { encoding: 'utf-8' });
|
||||
} catch (err) {
|
||||
if (err.errno !== -2) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAndStoreFile(code, date) {
|
||||
let res = await fetch(
|
||||
`https://api.github.com/repos/frappe/books/contents/translations/${code}.csv`
|
||||
);
|
||||
|
||||
let contents = undefined;
|
||||
if (res.status === 200) {
|
||||
const resJson = await res.json();
|
||||
contents = Buffer.from(resJson.content, 'base64').toString();
|
||||
} else {
|
||||
res = await fetch(
|
||||
`https://raw.githubusercontent.com/frappe/books/master/translations/${code}.csv`
|
||||
);
|
||||
}
|
||||
|
||||
if (!contents && res.status === 200) {
|
||||
contents = await res.text();
|
||||
}
|
||||
|
||||
if (!date && contents) {
|
||||
date = await getLastUpdated(code);
|
||||
}
|
||||
|
||||
if (contents) {
|
||||
contents = [date.toISOString(), contents].join('\n');
|
||||
await storeFile(code, contents);
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
async function getUpdatedContent(code, contents) {
|
||||
const [shouldUpdate, date] = await shouldUpdateFile(code, contents);
|
||||
if (!shouldUpdate) {
|
||||
return contents;
|
||||
}
|
||||
|
||||
return await fetchAndStoreFile(code, date);
|
||||
}
|
||||
|
||||
async function shouldUpdateFile(code, contents) {
|
||||
const date = await getLastUpdated(code);
|
||||
const oldDate = new Date(contents.split('\n')[0]);
|
||||
const shouldUpdate = date > oldDate || +oldDate === VALENTINES_DAY;
|
||||
|
||||
return [shouldUpdate, date];
|
||||
}
|
||||
|
||||
async function getLastUpdated(code) {
|
||||
const url = `https://api.github.com/repos/frappe/books/commits?path=translations%2F${code}.csv&page=1&per_page=1`;
|
||||
const resJson = await fetch(url).then((res) => res.json());
|
||||
|
||||
try {
|
||||
return new Date(resJson[0].commit.author.date);
|
||||
} catch {
|
||||
return new Date(VALENTINES_DAY);
|
||||
}
|
||||
}
|
||||
|
||||
function getFilePath(code) {
|
||||
return path.resolve(process.resourcesPath, 'translations', `${code}.csv`);
|
||||
}
|
||||
|
||||
function throwCouldNotFetchFile(code) {
|
||||
throw new Error(`Could not fetch translations for '${code}'.`);
|
||||
}
|
||||
|
||||
async function storeFile(code, contents) {
|
||||
const filePath = getFilePath(code);
|
||||
const dirname = path.dirname(filePath);
|
||||
await fs.mkdir(dirname, { recursive: true });
|
||||
await fs.writeFile(filePath, contents, { encoding: 'utf-8' });
|
||||
}
|
||||
|
||||
module.exports = { getLanguageMap };
|
@ -6,7 +6,7 @@ import regionalModelUpdates from '../models/regionalModelUpdates';
|
||||
import postStart, { setCurrencySymbols } from '../server/postStart';
|
||||
import { DB_CONN_FAILURE } from './messages';
|
||||
import runMigrate from './migrate';
|
||||
import { callInitializeMoneyMaker, getSavePath } from './utils';
|
||||
import { callInitializeMoneyMaker, getSavePath, setLanguageMap } from './utils';
|
||||
|
||||
export async function createNewDatabase() {
|
||||
const { canceled, filePath } = await getSavePath('books', 'db');
|
||||
|
7
src/languageCodeMap.js
Normal file
7
src/languageCodeMap.js
Normal file
@ -0,0 +1,7 @@
|
||||
// Language: Language Code in books/translations
|
||||
export const languageCodeMap = {
|
||||
English: 'en',
|
||||
French: 'fr',
|
||||
German: 'de',
|
||||
Portuguese: 'pt',
|
||||
};
|
15
src/main.js
15
src/main.js
@ -4,25 +4,26 @@ import { createApp } from 'vue';
|
||||
import models from '../models';
|
||||
import App from './App';
|
||||
import FeatherIcon from './components/FeatherIcon';
|
||||
import config from './config';
|
||||
import { getErrorHandled, handleError } from './errorHandling';
|
||||
import { IPC_CHANNELS, IPC_MESSAGES } from './messages';
|
||||
import router from './router';
|
||||
import { outsideClickDirective } from './ui';
|
||||
import { showToast, stringifyCircular } from './utils';
|
||||
|
||||
import { setLanguageMap, showToast, stringifyCircular } from './utils';
|
||||
(async () => {
|
||||
const language = config.get('language');
|
||||
if (language) {
|
||||
await setLanguageMap(language);
|
||||
}
|
||||
|
||||
frappe.isServer = true;
|
||||
frappe.isElectron = true;
|
||||
frappe.initializeAndRegister(models);
|
||||
frappe.initializeAndRegister(models, language);
|
||||
frappe.fetch = window.fetch.bind();
|
||||
|
||||
ipcRenderer.send = getErrorHandled(ipcRenderer.send);
|
||||
ipcRenderer.invoke = getErrorHandled(ipcRenderer.invoke);
|
||||
|
||||
frappe.events.on('reload-main-window', () => {
|
||||
ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
|
||||
});
|
||||
|
||||
window.frappe = frappe;
|
||||
window.frappe.store = {};
|
||||
|
||||
|
@ -23,6 +23,7 @@ export const IPC_ACTIONS = {
|
||||
SAVE_DATA: 'save-data',
|
||||
SHOW_ERROR: 'show-error',
|
||||
SEND_ERROR: 'send-error',
|
||||
GET_LANGUAGE_MAP: 'get-language-map',
|
||||
CHECK_FOR_UPDATES: 'check-for-updates',
|
||||
};
|
||||
|
||||
|
@ -149,6 +149,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="w-full flex justify-end absolute px-8"
|
||||
style="top: 100%; transform: translateY(-175%)"
|
||||
>
|
||||
<LanguageSelector class="w-28" input-class="text-base" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -160,6 +166,7 @@ import { DB_CONN_FAILURE, IPC_ACTIONS } from '../messages';
|
||||
|
||||
import { createNewDatabase, connectToLocalDatabase } from '@/initialization';
|
||||
import { showErrorDialog } from '../errorHandling';
|
||||
import LanguageSelector from '@/components/Controls/LanguageSelector.vue';
|
||||
|
||||
export default {
|
||||
name: 'DatabaseSelector',
|
||||
@ -211,18 +218,15 @@ export default {
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingDatabase = true;
|
||||
const { connectionSuccess, reason } = await connectToLocalDatabase(
|
||||
filePath
|
||||
);
|
||||
this.loadingDatabase = false;
|
||||
|
||||
if (connectionSuccess) {
|
||||
this.$emit('database-connect');
|
||||
return;
|
||||
}
|
||||
|
||||
const title = this.t`DB Connection Error`;
|
||||
let content =
|
||||
this.t`Please select an existing database or create a new one.` +
|
||||
@ -231,7 +235,6 @@ export default {
|
||||
content = this
|
||||
.t`Can't open database file: ${filePath}, please create a new file.`;
|
||||
}
|
||||
|
||||
await showErrorDialog(title, content);
|
||||
},
|
||||
getFileLastModified(filePath) {
|
||||
@ -239,5 +242,6 @@ export default {
|
||||
return DateTime.fromJSDate(stats.mtime).toRelative();
|
||||
},
|
||||
},
|
||||
components: { LanguageSelector },
|
||||
};
|
||||
</script>
|
||||
|
@ -22,13 +22,6 @@
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
<div
|
||||
id="toast-container"
|
||||
class="absolute bottom-0 flex flex-col items-center mb-3"
|
||||
style="width: calc(100% - 12rem)"
|
||||
>
|
||||
<div id="toast-target" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -63,6 +63,8 @@ import StatusBadge from '@/components/StatusBadge';
|
||||
import { callInitializeMoneyMaker } from '../../utils';
|
||||
import { showToast } from '../../utils';
|
||||
import { h, markRaw } from 'vue';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { IPC_MESSAGES } from '@/messages';
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
@ -113,9 +115,12 @@ export default {
|
||||
fieldnames.includes('displayPrecision') ||
|
||||
fieldnames.includes('hideGetStarted')
|
||||
) {
|
||||
callInitializeMoneyMaker(undefined, true);
|
||||
this.showReloadToast();
|
||||
}
|
||||
|
||||
if (fieldnames.includes('displayPrecision')) {
|
||||
callInitializeMoneyMaker(undefined, true);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showReloadToast() {
|
||||
@ -124,7 +129,7 @@ export default {
|
||||
actionText: frappe.t`Reload App`,
|
||||
type: 'info',
|
||||
action: async () => {
|
||||
frappe.events.trigger('reload-main-window');
|
||||
ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW)
|
||||
},
|
||||
});
|
||||
},
|
||||
|
@ -8,9 +8,10 @@
|
||||
:emit-change="true"
|
||||
@change="forwardChangeEvent"
|
||||
/>
|
||||
<div class="flex flex-row justify-end my-4">
|
||||
<div class="flex flex-row justify-between my-4">
|
||||
<LanguageSelector class="text-sm" input-class="px-4 py-1.5"/>
|
||||
<button
|
||||
class="text-gray-900 text-sm hover:bg-gray-100 rounded-md px-4 py-1.5"
|
||||
class="text-gray-900 text-sm hover:bg-gray-200 rounded-md px-4 py-1.5"
|
||||
@click="checkForUpdates(true)"
|
||||
>
|
||||
Check for Updates
|
||||
@ -23,11 +24,13 @@
|
||||
import frappe from 'frappe';
|
||||
import TwoColumnForm from '@/components/TwoColumnForm';
|
||||
import { checkForUpdates } from '@/utils';
|
||||
import LanguageSelector from '@/components/Controls/LanguageSelector.vue';
|
||||
|
||||
export default {
|
||||
name: 'TabSystem',
|
||||
components: {
|
||||
TwoColumnForm,
|
||||
LanguageSelector,
|
||||
},
|
||||
emits: ['change'],
|
||||
data() {
|
||||
|
@ -1,94 +1,120 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex-1 py-10 bg-white"
|
||||
:class="{
|
||||
'window-drag': platform !== 'Windows',
|
||||
}"
|
||||
>
|
||||
<div class="px-12">
|
||||
<h1 class="text-2xl font-semibold">{{ t`Setup your organization` }}</h1>
|
||||
</div>
|
||||
<div class="px-8 mt-5 window-no-drag" v-if="doc">
|
||||
<div class="flex items-center px-6 py-5 mb-4 border bg-brand rounded-xl">
|
||||
<FormControl
|
||||
:df="meta.getField('companyLogo')"
|
||||
:value="doc.companyLogo"
|
||||
@change="(value) => setValue('companyLogo', value)"
|
||||
/>
|
||||
<div class="ml-2">
|
||||
<FormControl
|
||||
ref="companyField"
|
||||
:df="meta.getField('companyName')"
|
||||
:value="doc.companyName"
|
||||
@change="(value) => setValue('companyName', value)"
|
||||
:input-class="
|
||||
(classes) => [
|
||||
'bg-transparent font-semibold text-xl text-white placeholder-blue-200 focus:outline-none focus:bg-blue-600 px-3 rounded py-1',
|
||||
]
|
||||
"
|
||||
:autofocus="true"
|
||||
/>
|
||||
<Popover placement="auto" :show-popup="Boolean(emailError)">
|
||||
<template #target>
|
||||
<div>
|
||||
<Slide
|
||||
@primary-clicked="handlePrimary"
|
||||
@secondary-clicked="handleSecondary"
|
||||
v-show="index === 0"
|
||||
>
|
||||
<template #title>
|
||||
{{ t`Select your language` }}
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex flex-col justify-center items-center h-96">
|
||||
<LanguageSelector class="w-40 mt-8" :dont-reload="true" />
|
||||
<p
|
||||
class="text-sm mt-2 hover:underline cursor-pointer text-gray-700"
|
||||
@click="openContributingTranslations"
|
||||
>
|
||||
{{ t`I can't find my language.` }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #secondaryButton>
|
||||
{{ t`Cancel` }}
|
||||
</template>
|
||||
<template #primaryButton>
|
||||
{{ t`Next` }}
|
||||
</template>
|
||||
</Slide>
|
||||
<Slide
|
||||
:primary-disabled="!valuesFilled || loading"
|
||||
@primary-clicked="handlePrimary"
|
||||
@secondary-clicked="handleSecondary"
|
||||
v-show="index === 1"
|
||||
>
|
||||
<template #title>
|
||||
{{ t`Setup your organization` }}
|
||||
</template>
|
||||
<template #content>
|
||||
<div v-if="doc">
|
||||
<div
|
||||
class="flex items-center px-6 py-5 mb-4 border bg-brand rounded-xl"
|
||||
>
|
||||
<FormControl
|
||||
:df="meta.getField('companyLogo')"
|
||||
:value="doc.companyLogo"
|
||||
@change="(value) => setValue('companyLogo', value)"
|
||||
/>
|
||||
<div class="ml-2">
|
||||
<FormControl
|
||||
:df="meta.getField('email')"
|
||||
:value="doc.email"
|
||||
@change="(value) => setValue('email', value)"
|
||||
ref="companyField"
|
||||
:df="meta.getField('companyName')"
|
||||
:value="doc.companyName"
|
||||
@change="(value) => setValue('companyName', value)"
|
||||
:input-class="
|
||||
(classes) => [
|
||||
'text-base bg-transparent text-white placeholder-blue-200 focus:bg-blue-600 focus:outline-none rounded px-3 py-1',
|
||||
'bg-transparent font-semibold text-xl text-white placeholder-blue-200 focus:outline-none focus:bg-blue-600 px-3 rounded py-1',
|
||||
]
|
||||
"
|
||||
:autofocus="true"
|
||||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="p-2 text-sm">
|
||||
{{ emailError }}
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
<Popover placement="auto" :show-popup="Boolean(emailError)">
|
||||
<template #target>
|
||||
<FormControl
|
||||
:df="meta.getField('email')"
|
||||
:value="doc.email"
|
||||
@change="(value) => setValue('email', value)"
|
||||
:input-class="
|
||||
(classes) => [
|
||||
'text-base bg-transparent text-white placeholder-blue-200 focus:bg-blue-600 focus:outline-none rounded px-3 py-1',
|
||||
]
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="p-2 text-sm">
|
||||
{{ emailError }}
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<TwoColumnForm :fields="fields" :doc="doc" />
|
||||
</div>
|
||||
</div>
|
||||
<TwoColumnForm :fields="fields" :doc="doc" />
|
||||
</div>
|
||||
<div class="flex justify-between px-8 mt-5 window-no-drag">
|
||||
<Button class="text-sm text-grey-900" @click="$emit('setup-canceled')"
|
||||
>Cancel</Button
|
||||
>
|
||||
<Button
|
||||
@click="submit"
|
||||
type="primary"
|
||||
class="text-sm text-white"
|
||||
:disabled="!valuesFilled || loading"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template #secondaryButton>{{ t`Back` }}</template>
|
||||
<template #primaryButton>{{ t`Submit` }}</template>
|
||||
</Slide>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import frappe from 'frappe';
|
||||
import TwoColumnForm from '@/components/TwoColumnForm';
|
||||
import FormControl from '@/components/Controls/FormControl';
|
||||
import Button from '@/components/Button';
|
||||
import setupCompany from './setupCompany';
|
||||
import Popover from '@/components/Popover';
|
||||
import config from '@/config';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { purgeCache, connectToLocalDatabase } from '@/initialization';
|
||||
import { showMessageDialog } from '@/utils';
|
||||
import { setLanguageMap, showMessageDialog } from '@/utils';
|
||||
import {
|
||||
handleErrorWithDialog,
|
||||
getErrorMessage,
|
||||
showErrorDialog,
|
||||
} from '../../errorHandling';
|
||||
import Slide from './Slide.vue';
|
||||
import LanguageSelector from '@/components/Controls/LanguageSelector.vue';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { IPC_MESSAGES } from '@/messages';
|
||||
|
||||
export default {
|
||||
name: 'SetupWizard',
|
||||
emits: ['setup-complete', 'setup-canceled'],
|
||||
data() {
|
||||
return {
|
||||
index: 0,
|
||||
doc: null,
|
||||
loading: false,
|
||||
valuesFilled: false,
|
||||
@ -104,16 +130,45 @@ export default {
|
||||
components: {
|
||||
TwoColumnForm,
|
||||
FormControl,
|
||||
Button,
|
||||
Popover,
|
||||
Slide,
|
||||
LanguageSelector,
|
||||
},
|
||||
async mounted() {
|
||||
if (config.get('language') !== undefined) {
|
||||
this.index = 1;
|
||||
}
|
||||
|
||||
this.doc = await frappe.newDoc({ doctype: 'SetupWizard' });
|
||||
this.doc.on('change', () => {
|
||||
this.valuesFilled = this.allValuesFilled();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
openContributingTranslations() {
|
||||
ipcRenderer.send(
|
||||
IPC_MESSAGES.OPEN_EXTERNAL,
|
||||
'https://github.com/frappe/books/wiki/Contributing-Translations'
|
||||
);
|
||||
},
|
||||
handlePrimary() {
|
||||
if (this.index === 0) {
|
||||
this.index = 1;
|
||||
} else if (this.index === 1) {
|
||||
this.submit();
|
||||
}
|
||||
},
|
||||
handleSecondary() {
|
||||
if (this.index === 1) {
|
||||
this.index = 0;
|
||||
} else if (this.index === 0) {
|
||||
this.$emit('setup-canceled');
|
||||
}
|
||||
},
|
||||
async selectLanguage(value) {
|
||||
const success = await setLanguageMap(value);
|
||||
this.setValue('language', value);
|
||||
},
|
||||
setValue(fieldname, value) {
|
||||
this.emailError = null;
|
||||
this.doc.set(fieldname, value).catch((e) => {
|
||||
|
45
src/pages/SetupWizard/Slide.vue
Normal file
45
src/pages/SetupWizard/Slide.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex-1 py-10 bg-white h-screen"
|
||||
:class="{
|
||||
'window-drag': platform !== 'Windows',
|
||||
}"
|
||||
>
|
||||
<div class="px-12">
|
||||
<h1 class="text-2xl font-semibold"><slot name="title"></slot></h1>
|
||||
</div>
|
||||
|
||||
<div class="px-8 mt-5 window-no-drag">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between px-8 mt-5 window-no-drag absolute w-full"
|
||||
style="top: 100%; transform: translateY(-260%)"
|
||||
>
|
||||
<Button class="text-sm text-grey-900" @click="$emit('secondary-clicked')">
|
||||
<slot name="secondaryButton"></slot>
|
||||
</Button>
|
||||
<Button
|
||||
@click="$emit('primary-clicked')"
|
||||
type="primary"
|
||||
class="text-sm text-white"
|
||||
:disabled="primaryDisabled"
|
||||
>
|
||||
<slot name="primaryButton"></slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Button from '@/components/Button.vue';
|
||||
|
||||
export default {
|
||||
emits: ['primary-clicked', 'secondary-clicked'],
|
||||
components: { Button },
|
||||
props: {
|
||||
usePrimary: { type: Boolean, default: true },
|
||||
primaryDisabled: { type: Boolean, default: false },
|
||||
},
|
||||
};
|
||||
</script>
|
@ -45,7 +45,7 @@ export default async function setupCompany(setupWizardValues) {
|
||||
await setupGlobalCurrencies(countryList);
|
||||
await setupChartOfAccounts(bankName, country);
|
||||
await setupRegionalChanges(country);
|
||||
updateCompanyNameInConfig();
|
||||
updateInitializationConfig();
|
||||
|
||||
await accountingSettings.update({ setupComplete: 1 });
|
||||
frappe.AccountingSettings = accountingSettings;
|
||||
@ -109,7 +109,7 @@ async function setupRegionalChanges(country) {
|
||||
await frappe.db.migrate();
|
||||
}
|
||||
|
||||
function updateCompanyNameInConfig() {
|
||||
function updateInitializationConfig(language) {
|
||||
let filePath = frappe.db.dbPath;
|
||||
let files = config.get('files', []);
|
||||
files.forEach((file) => {
|
||||
|
59
src/utils.js
59
src/utils.js
@ -4,9 +4,13 @@ import router from '@/router';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import frappe, { t } from 'frappe';
|
||||
import { isPesa } from 'frappe/utils';
|
||||
import { DEFAULT_LANGUAGE } from 'frappe/utils/consts';
|
||||
import { setLanguageMapOnTranslationString } from 'frappe/utils/translation';
|
||||
import lodash from 'lodash';
|
||||
import { createApp, h } from 'vue';
|
||||
import config from './config';
|
||||
import { handleErrorWithDialog } from './errorHandling';
|
||||
import { languageCodeMap } from './languageCodeMap';
|
||||
import { IPC_ACTIONS, IPC_MESSAGES } from './messages';
|
||||
|
||||
export async function showMessageDialog({
|
||||
@ -455,8 +459,57 @@ export function stringifyCircular(
|
||||
});
|
||||
}
|
||||
|
||||
window.showToast = showToast;
|
||||
|
||||
export function checkForUpdates(force = false) {
|
||||
export async function checkForUpdates(force = false) {
|
||||
ipcRenderer.invoke(IPC_ACTIONS.CHECK_FOR_UPDATES, force);
|
||||
await setLanguageMap();
|
||||
}
|
||||
|
||||
async function fetchAndSetLanguageMap(code) {
|
||||
const { success, message, languageMap } = await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.GET_LANGUAGE_MAP,
|
||||
code
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
showToast({ type: 'error', message });
|
||||
} else {
|
||||
setLanguageMapOnTranslationString(languageMap);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
export async function setLanguageMap(initLanguage, dontReload = false) {
|
||||
const oldLanguage = config.get('language');
|
||||
initLanguage ??= oldLanguage;
|
||||
const [code, language, usingDefault] = getLanguageCode(
|
||||
initLanguage,
|
||||
oldLanguage
|
||||
);
|
||||
|
||||
let success = true;
|
||||
if (code === 'en') {
|
||||
setLanguageMapOnTranslationString(undefined);
|
||||
} else {
|
||||
success = await fetchAndSetLanguageMap(code);
|
||||
}
|
||||
|
||||
if (success && !usingDefault) {
|
||||
config.set('language', language);
|
||||
}
|
||||
|
||||
if (!dontReload && success && initLanguage !== oldLanguage) {
|
||||
await ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
function getLanguageCode(initLanguage, oldLanguage) {
|
||||
let language = initLanguage ?? oldLanguage;
|
||||
let usingDefault = false;
|
||||
if (!language) {
|
||||
language = DEFAULT_LANGUAGE;
|
||||
usingDefault = true;
|
||||
}
|
||||
return [languageCodeMap[language], language, usingDefault];
|
||||
}
|
||||
|
@ -35,7 +35,7 @@
|
||||
`An error occurred.`,`Es ist ein Fehler aufgetreten.`
|
||||
`Application of Funds (Assets)`,`Verwendung der Mittel (Aktiva)`
|
||||
`Are you sure you want to cancel ${0} ${1}?`,`Sind Sie sicher, dass Sie ${0} ${1} stornieren wollen?`
|
||||
`Are you sure you want to delete ${0} "${1}"?`,`Sind Sie sicher, dass Sie ${0} "${1}" löschen wollen?`
|
||||
`Are you sure you want to delete ${0} ${1}?`,`Sind Sie sicher, dass Sie ${0} ${1} löschen wollen?`
|
||||
`Are you sure you want to submit this Bill?`,`Sind Sie sicher, dass Sie diesen Gesetzentwurf einreichen wollen?`
|
||||
`Are you sure you want to submit this Invoice?`,`Sind Sie sicher, dass Sie diese Rechnung einreichen möchten?`
|
||||
`Are you sure you want to submit this Journal Entry?`,`Sind Sie sicher, dass Sie diesen Journaleintrag abschicken wollen?`
|
||||
@ -304,7 +304,7 @@
|
||||
`Place of supply`,`Ort der Lieferung`
|
||||
`Plants and Machineries`,`Anlagen und Maschinen`
|
||||
`Please fill all values`,`Bitte alle Werte ausfüllen`
|
||||
`Please select an existing database or create a new one. reason: ${0}, filePath: ${1}`,`Bitte wählen Sie eine vorhandene Datenbank oder erstellen Sie eine neue. reason: ${0}, filePath: ${1}`
|
||||
`Please select an existing database or create a new one.`,`Bitte wählen Sie eine vorhandene Datenbank oder erstellen Sie eine neue.`
|
||||
`Please set GSTIN in General Settings.`,`Bitte stellen Sie die GSTIN in den Allgemeinen Einstellungen ein.`
|
||||
`Postal Code`,`Postleitzahl`
|
||||
`Postal Expenses`,`Postkosten`
|
||||
@ -476,5 +476,4 @@
|
||||
`Year`,`Jahr`
|
||||
`Yes`,`Ja`
|
||||
`Your Name`,`Ihr Name`
|
||||
`name`,`Name`
|
||||
`reason: ${0}, filePath: ${1}`,`Grund: ${0}, Dateipfad: ${1}`
|
||||
`name`,`Name`
|
Can't render this file because it has a wrong number of fields in line 33.
|
@ -35,7 +35,7 @@
|
||||
`An error occurred.`,`Une erreur s'est produite.`
|
||||
`Application of Funds (Assets)`,`Application des fonds (actifs)`
|
||||
`Are you sure you want to cancel ${0} ${1}?`,`Vous êtes sûr de vouloir annuler ${0} ${1} ?`
|
||||
`Are you sure you want to delete ${0} "${1}"?`,`Etes-vous sûr de vouloir supprimer ${0} "${1}" ?`
|
||||
`Are you sure you want to delete ${0} ${1}?`,`Etes-vous sûr de vouloir supprimer ${0} ${1} ?`
|
||||
`Are you sure you want to submit this Bill?`,`Êtes-vous sûr de vouloir soumettre ce projet de loi ?`
|
||||
`Are you sure you want to submit this Invoice?`,`Êtes-vous sûr de vouloir soumettre cette facture ?`
|
||||
`Are you sure you want to submit this Journal Entry?`,`Êtes-vous sûr de vouloir soumettre cette entrée de journal ?`
|
||||
@ -304,7 +304,7 @@
|
||||
`Place of supply`,`Lieu d'approvisionnement`
|
||||
`Plants and Machineries`,`Usines et machineries`
|
||||
`Please fill all values`,`Veuillez remplir toutes les valeurs`
|
||||
`Please select an existing database or create a new one. reason: ${0}, filePath: ${1}`,`Veuillez sélectionner une base de données existante ou en créer une nouvelle. reason : ${0}, filePath : ${1}`
|
||||
`Please select an existing database or create a new one.`,`Veuillez sélectionner une base de données existante ou en créer une nouvelle.`
|
||||
`Please set GSTIN in General Settings.`,`Veuillez définir le GSTIN dans les paramètres généraux.`
|
||||
`Postal Code`,`Code postal`
|
||||
`Postal Expenses`,`Dépenses postales`
|
||||
@ -476,5 +476,4 @@
|
||||
`Year`,`Année`
|
||||
`Yes`,`Oui`
|
||||
`Your Name`,`Votre nom`
|
||||
`name`,`nom`
|
||||
`reason: ${0}, filePath: ${1}`,`raison : ${0}, filePath : ${1}`
|
||||
`name`,`nom`
|
Can't render this file because it contains an unexpected character in line 38 and column 39.
|
@ -35,7 +35,7 @@
|
||||
`An error occurred.`,`Ocorreu um erro.`
|
||||
`Application of Funds (Assets)`,`Aplicação de Fundos (Activos)`
|
||||
`Are you sure you want to cancel ${0} ${1}?`,`Tem a certeza de que quer cancelar ${0} ${1}?`
|
||||
`Are you sure you want to delete ${0} "${1}"?`,`Tem a certeza que quer apagar ${0} "${1}"?`
|
||||
`Are you sure you want to delete ${0} ${1}?`,`Tem a certeza que quer apagar ${0} ${1}?`
|
||||
`Are you sure you want to submit this Bill?`,`Tem a certeza de que quer apresentar esta Proposta de Lei?`
|
||||
`Are you sure you want to submit this Invoice?`,`Tem a certeza de que quer apresentar esta factura?`
|
||||
`Are you sure you want to submit this Journal Entry?`,`Tem a certeza de que quer submeter esta Entrada no Jornal?`
|
||||
@ -304,7 +304,7 @@
|
||||
`Place of supply`,`Local de fornecimento`
|
||||
`Plants and Machineries`,`Fábricas e maquinarias`
|
||||
`Please fill all values`,`Por favor preencha todos os valores`
|
||||
`Please select an existing database or create a new one. reason: ${0}, filePath: ${1}`,`Por favor seleccione uma base de dados existente ou crie uma nova. razão: ${0}, filePath: ${1}`
|
||||
`Please select an existing database or create a new one.`,`Por favor seleccione uma base de dados existente ou crie uma nova.`
|
||||
`Please set GSTIN in General Settings.`,`Por favor, defina GSTIN em Definições Gerais.`
|
||||
`Postal Code`,`Código Postal`
|
||||
`Postal Expenses`,`Despesas de correio`
|
||||
@ -476,5 +476,4 @@
|
||||
`Year`,`Ano`
|
||||
`Yes`,`Sim`
|
||||
`Your Name`,`O seu nome`
|
||||
`name`,`nome`
|
||||
`reason: ${0}, filePath: ${1}`,`razão: ${0}, filePath: ${1}`
|
||||
`name`,`nome`
|
Can't render this file because it contains an unexpected character in line 38 and column 39.
|
@ -7965,7 +7965,7 @@ node-emoji@^1.11.0:
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
node-fetch@^2.6.1:
|
||||
node-fetch@2, node-fetch@^2.6.1:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
|
Loading…
Reference in New Issue
Block a user